diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go
index 6d45f4d5627..06fc3b52ac6 100644
--- a/pkg/services/folder/folderimpl/folder.go
+++ b/pkg/services/folder/folderimpl/folder.go
@@ -121,6 +121,37 @@ func (s *Service) DBMigration(db db.DB) {
 	s.log.Debug("syncing dashboard and folder tables finished")
 }
 
+func (s *Service) GetFolders(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) {
+	if q.SignedInUser == nil {
+		return nil, folder.ErrBadRequest.Errorf("missing signed in user")
+	}
+
+	qry := NewGetFoldersQuery(q)
+	permissions := q.SignedInUser.GetPermissions()
+	folderPermissions := permissions[dashboards.ActionFoldersRead]
+	qry.ancestorUIDs = make([]string, 0, len(folderPermissions))
+	for _, p := range folderPermissions {
+		if p == dashboards.ScopeFoldersAll {
+			// no need to query for folders with permissions
+			// the user has permission to access all folders
+			qry.ancestorUIDs = nil
+			break
+		}
+		if folderUid, found := strings.CutPrefix(p, dashboards.ScopeFoldersPrefix); found {
+			if !slices.Contains(qry.ancestorUIDs, folderUid) {
+				qry.ancestorUIDs = append(qry.ancestorUIDs, folderUid)
+			}
+		}
+	}
+
+	dashFolders, err := s.store.GetFolders(ctx, qry)
+	if err != nil {
+		return nil, folder.ErrInternal.Errorf("failed to fetch subfolders: %w", err)
+	}
+
+	return dashFolders, nil
+}
+
 func (s *Service) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
 	if q.SignedInUser == nil {
 		return nil, folder.ErrBadRequest.Errorf("missing signed in user")
@@ -357,7 +388,7 @@ func (s *Service) getAvailableNonRootFolders(ctx context.Context, orgID int64, u
 		return nonRootFolders, nil
 	}
 
-	dashFolders, err := s.store.GetFolders(ctx, orgID, folderUids)
+	dashFolders, err := s.store.GetFolders(ctx, NewGetFoldersQuery(folder.GetFoldersQuery{OrgID: orgID, UIDs: folderUids}))
 	if err != nil {
 		return nil, folder.ErrInternal.Errorf("failed to fetch subfolders: %w", err)
 	}
diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go
index 86371a13fbf..1b9806292b7 100644
--- a/pkg/services/folder/folderimpl/folder_test.go
+++ b/pkg/services/folder/folderimpl/folder_test.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"fmt"
 	"math/rand"
+	"strings"
 	"testing"
 	"time"
 
@@ -420,11 +421,11 @@ func TestIntegrationNestedFolderService(t *testing.T) {
 			lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOn)
 			require.NoError(t, err)
 
-			ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "getDescendantCountsOn", createCmd)
+			ancestors := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "getDescendantCountsOn", createCmd)
 
-			parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0])
+			parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[0].UID)
 			require.NoError(t, err)
-			subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[1])
+			subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[1].UID)
 			require.NoError(t, err)
 			// nolint:staticcheck
 			_ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in parent", orgID, parent.ID, parent.UID, "prod")
@@ -443,7 +444,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
 			require.NoError(t, err)
 
 			countCmd := folder.GetDescendantCountsQuery{
-				UID:          &ancestorUIDs[0],
+				UID:          &ancestors[0].UID,
 				OrgID:        orgID,
 				SignedInUser: &signedInUser,
 			}
@@ -456,8 +457,8 @@ func TestIntegrationNestedFolderService(t *testing.T) {
 
 			t.Cleanup(func() {
 				guardian.New = origNewGuardian
-				for _, uid := range ancestorUIDs {
-					err := serviceWithFlagOn.store.Delete(context.Background(), uid, orgID)
+				for _, ancestor := range ancestors {
+					err := serviceWithFlagOn.store.Delete(context.Background(), ancestor.UID, orgID)
 					assert.NoError(t, err)
 				}
 			})
@@ -500,11 +501,11 @@ func TestIntegrationNestedFolderService(t *testing.T) {
 			lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOff)
 			require.NoError(t, err)
 
-			ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "getDescendantCountsOff", createCmd)
+			ancestors := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "getDescendantCountsOff", createCmd)
 
-			parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0])
+			parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[0].UID)
 			require.NoError(t, err)
-			subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[1])
+			subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[1].UID)
 			require.NoError(t, err)
 			// nolint:staticcheck
 			_ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in parent", orgID, parent.ID, parent.UID, "prod")
@@ -523,7 +524,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
 			require.NoError(t, err)
 
 			countCmd := folder.GetDescendantCountsQuery{
-				UID:          &ancestorUIDs[0],
+				UID:          &ancestors[0].UID,
 				OrgID:        orgID,
 				SignedInUser: &signedInUser,
 			}
@@ -536,8 +537,8 @@ func TestIntegrationNestedFolderService(t *testing.T) {
 
 			t.Cleanup(func() {
 				guardian.New = origNewGuardian
-				for _, uid := range ancestorUIDs {
-					err := serviceWithFlagOn.store.Delete(context.Background(), uid, orgID)
+				for _, ancestor := range ancestors {
+					err := serviceWithFlagOn.store.Delete(context.Background(), ancestor.UID, orgID)
 					assert.NoError(t, err)
 				}
 			})
@@ -638,9 +639,9 @@ func TestIntegrationNestedFolderService(t *testing.T) {
 				alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, dashSrv, ac)
 				require.NoError(t, err)
 
-				ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, tc.depth, tc.prefix, createCmd)
+				ancestors := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, tc.depth, tc.prefix, createCmd)
 
-				parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0])
+				parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[0].UID)
 				require.NoError(t, err)
 				_ = createRule(t, alertStore, parent.UID, "parent alert")
 
@@ -649,7 +650,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
 					subPanel  model.LibraryElementDTO
 				)
 				if tc.depth > 1 {
-					subfolder, err = serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[1])
+					subfolder, err = serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[1].UID)
 					require.NoError(t, err)
 					_ = createRule(t, alertStore, subfolder.UID, "sub alert")
 					// nolint:staticcheck
@@ -663,7 +664,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
 				require.NoError(t, err)
 
 				deleteCmd := folder.DeleteFolderCommand{
-					UID:              ancestorUIDs[0],
+					UID:              ancestors[0].UID,
 					OrgID:            orgID,
 					SignedInUser:     &signedInUser,
 					ForceDeleteRules: tc.forceDelete,
@@ -672,12 +673,12 @@ func TestIntegrationNestedFolderService(t *testing.T) {
 				err = tc.service.Delete(context.Background(), &deleteCmd)
 				require.ErrorIs(t, err, tc.deletionErr)
 
-				for i, uid := range ancestorUIDs {
+				for i, ancestor := range ancestors {
 					// dashboard table
-					_, err := tc.service.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, uid)
+					_, err := tc.service.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestor.UID)
 					require.ErrorIs(t, err, tc.dashboardErr)
 					// folder table
-					_, err = tc.service.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestorUIDs[i], OrgID: orgID})
+					_, err = tc.service.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestors[i].UID, OrgID: orgID})
 					require.ErrorIs(t, err, tc.folderErr)
 				}
 
@@ -1325,12 +1326,12 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) {
 			CanViewValue: true,
 		})
 
-		ancestorUIDsFolderWithPermissions := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "withPermissions", createCmd)
-		ancestorUIDsFolderWithoutPermissions := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "withoutPermissions", createCmd)
+		ancestorFoldersWithPermissions := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "withPermissions", createCmd)
+		ancestorFoldersWithoutPermissions := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "withoutPermissions", createCmd)
 
-		parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDsFolderWithoutPermissions[0])
+		parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorFoldersWithoutPermissions[0].UID)
 		require.NoError(t, err)
-		subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDsFolderWithoutPermissions[1])
+		subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorFoldersWithoutPermissions[1].UID)
 		require.NoError(t, err)
 		// nolint:staticcheck
 		dash1 := insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in parent", orgID, parent.ID, parent.UID, "prod")
@@ -1341,19 +1342,19 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) {
 			CanSaveValue: true,
 			CanViewValue: true,
 			CanViewUIDs: []string{
-				ancestorUIDsFolderWithPermissions[0],
-				ancestorUIDsFolderWithPermissions[1],
-				ancestorUIDsFolderWithoutPermissions[1],
+				ancestorFoldersWithPermissions[0].UID,
+				ancestorFoldersWithPermissions[1].UID,
+				ancestorFoldersWithoutPermissions[1].UID,
 				dash1.UID,
 				dash2.UID,
 			},
 		})
 		signedInUser.Permissions[orgID][dashboards.ActionFoldersRead] = []string{
-			dashboards.ScopeFoldersProvider.GetResourceScopeUID(ancestorUIDsFolderWithPermissions[0]),
+			dashboards.ScopeFoldersProvider.GetResourceScopeUID(ancestorFoldersWithPermissions[0].UID),
 			// Add permission to the subfolder of folder with permission (to check deduplication)
-			dashboards.ScopeFoldersProvider.GetResourceScopeUID(ancestorUIDsFolderWithPermissions[1]),
+			dashboards.ScopeFoldersProvider.GetResourceScopeUID(ancestorFoldersWithPermissions[1].UID),
 			// Add permission to the subfolder of folder without permission
-			dashboards.ScopeFoldersProvider.GetResourceScopeUID(ancestorUIDsFolderWithoutPermissions[1]),
+			dashboards.ScopeFoldersProvider.GetResourceScopeUID(ancestorFoldersWithoutPermissions[1].UID),
 		}
 		signedInUser.Permissions[orgID][dashboards.ActionDashboardsRead] = []string{
 			dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash1.UID),
@@ -1374,8 +1375,8 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) {
 
 		require.NoError(t, err)
 		require.Len(t, sharedFolders, 1)
-		require.Contains(t, sharedFoldersUIDs, ancestorUIDsFolderWithoutPermissions[1])
-		require.NotContains(t, sharedFoldersUIDs, ancestorUIDsFolderWithPermissions[1])
+		require.Contains(t, sharedFoldersUIDs, ancestorFoldersWithoutPermissions[1].UID)
+		require.NotContains(t, sharedFoldersUIDs, ancestorFoldersWithPermissions[1].UID)
 
 		sharedDashboards, err := dashboardService.GetDashboardsSharedWithUser(context.Background(), &signedInUser)
 		sharedDashboardsUIDs := make([]string, 0)
@@ -1390,28 +1391,236 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) {
 
 		t.Cleanup(func() {
 			guardian.New = origNewGuardian
-			for _, uid := range ancestorUIDsFolderWithPermissions {
-				err := serviceWithFlagOn.store.Delete(context.Background(), uid, orgID)
+			for _, ancestor := range ancestorFoldersWithPermissions {
+				err := serviceWithFlagOn.store.Delete(context.Background(), ancestor.UID, orgID)
 				assert.NoError(t, err)
 			}
 		})
 		t.Cleanup(func() {
 			guardian.New = origNewGuardian
-			for _, uid := range ancestorUIDsFolderWithoutPermissions {
-				err := serviceWithFlagOn.store.Delete(context.Background(), uid, orgID)
+			for _, ancestor := range ancestorFoldersWithoutPermissions {
+				err := serviceWithFlagOn.store.Delete(context.Background(), ancestor.UID, orgID)
 				assert.NoError(t, err)
 			}
 		})
 	})
+
+	t.Run("Should get org folders visible", func(t *testing.T) {
+		depth := 3
+		origNewGuardian := guardian.New
+		guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
+			CanSaveValue: true,
+			CanViewValue: true,
+		})
+
+		// create folder sctructure like this:
+		// tree1-folder-0
+		// └──tree1-folder-1
+		// 	└──tree1-folder-2
+		// tree2-folder-0
+		//  └──tree2-folder-1
+		// 	 └──tree2-folder-2
+		tree1 := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "tree1-", createCmd)
+		tree2 := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "tree2-", createCmd)
+
+		signedInUser.Permissions[orgID][dashboards.ActionFoldersRead] = []string{
+			// Add permission to tree1-folder-0
+			dashboards.ScopeFoldersProvider.GetResourceScopeUID(tree1[0].UID),
+			// Add permission to the subfolder of folder with permission (tree1-folder-1) to check deduplication
+			dashboards.ScopeFoldersProvider.GetResourceScopeUID(tree1[1].UID),
+			// Add permission to the subfolder of folder without permission (tree2-folder-1)
+			dashboards.ScopeFoldersProvider.GetResourceScopeUID(tree2[1].UID),
+		}
+
+		t.Cleanup(func() {
+			guardian.New = origNewGuardian
+			for _, f := range tree1 {
+				err := serviceWithFlagOn.store.Delete(context.Background(), f.UID, orgID)
+				assert.NoError(t, err)
+			}
+			for _, f := range tree2 {
+				err := serviceWithFlagOn.store.Delete(context.Background(), f.UID, orgID)
+				assert.NoError(t, err)
+			}
+		})
+
+		testCases := []struct {
+			name     string
+			cmd      folder.GetFoldersQuery
+			expected []*folder.Folder
+		}{
+			{
+				name: "Should get all org folders visible to the user",
+				cmd: folder.GetFoldersQuery{
+					OrgID:        orgID,
+					SignedInUser: &signedInUser,
+				},
+				expected: []*folder.Folder{
+					{
+						UID: tree1[0].UID,
+					},
+					{
+						UID: tree1[1].UID,
+					},
+					{
+						UID: tree1[2].UID,
+					},
+					{
+						UID: tree2[1].UID,
+					},
+					{
+						UID: tree2[2].UID,
+					},
+				},
+			},
+			{
+				name: "Should get all org folders visible to the user with fullpath",
+				cmd: folder.GetFoldersQuery{
+					OrgID:        orgID,
+					WithFullpath: true,
+					SignedInUser: &signedInUser,
+				},
+				expected: []*folder.Folder{
+					{
+						UID:      tree1[0].UID,
+						Fullpath: "tree1-folder-0",
+					},
+					{
+						UID:      tree1[1].UID,
+						Fullpath: "tree1-folder-0/tree1-folder-1",
+					},
+					{
+						UID:      tree1[2].UID,
+						Fullpath: "tree1-folder-0/tree1-folder-1/tree1-folder-2",
+					},
+					{
+						UID:      tree2[1].UID,
+						Fullpath: "tree2-folder-0/tree2-folder-1",
+					},
+					{
+						UID:      tree2[2].UID,
+						Fullpath: "tree2-folder-0/tree2-folder-1/tree2-folder-2",
+					},
+				},
+			},
+			{
+				name: "Should get all org folders visible to the user with fullpath UIDs",
+				cmd: folder.GetFoldersQuery{
+					OrgID:            orgID,
+					WithFullpathUIDs: true,
+					SignedInUser:     &signedInUser,
+				},
+				expected: []*folder.Folder{
+					{
+						UID:          tree1[0].UID,
+						FullpathUIDs: strings.Join([]string{tree1[0].UID}, "/"),
+					},
+					{
+						UID:          tree1[1].UID,
+						FullpathUIDs: strings.Join([]string{tree1[0].UID, tree1[1].UID}, "/"),
+					},
+					{
+						UID:          tree1[2].UID,
+						FullpathUIDs: strings.Join([]string{tree1[0].UID, tree1[1].UID, tree1[2].UID}, "/"),
+					},
+					{
+						UID:          tree2[1].UID,
+						FullpathUIDs: strings.Join([]string{tree2[0].UID, tree2[1].UID}, "/"),
+					},
+					{
+						UID:          tree2[2].UID,
+						FullpathUIDs: strings.Join([]string{tree2[0].UID, tree2[1].UID, tree2[2].UID}, "/"),
+					},
+				},
+			},
+			{
+				name: "Should get specific org folders visible to the user",
+				cmd: folder.GetFoldersQuery{
+					OrgID:        orgID,
+					UIDs:         []string{tree1[0].UID, tree2[0].UID, tree2[1].UID},
+					SignedInUser: &signedInUser,
+				},
+				expected: []*folder.Folder{
+					{
+						UID: tree1[0].UID,
+					},
+					{
+						UID: tree2[1].UID,
+					},
+				},
+			},
+			{
+				name: "Should get all org folders visible to the user with admin permissions",
+				cmd: folder.GetFoldersQuery{
+					OrgID:        orgID,
+					SignedInUser: &signedInAdminUser,
+				},
+				expected: []*folder.Folder{
+					{
+						UID:          tree1[0].UID,
+						Fullpath:     "tree1-folder-0",
+						FullpathUIDs: strings.Join([]string{tree1[0].UID}, "/"),
+					},
+					{
+						UID:          tree1[1].UID,
+						Fullpath:     "tree1-folder-0/tree1-folder-1",
+						FullpathUIDs: strings.Join([]string{tree1[0].UID, tree1[1].UID}, "/"),
+					},
+					{
+						UID:      tree1[2].UID,
+						Fullpath: "tree1-folder-0/tree1-folder-1/tree1-folder-2",
+					},
+					{
+						UID:          tree2[0].UID,
+						Fullpath:     "tree2-folder-0",
+						FullpathUIDs: strings.Join([]string{tree2[0].UID}, "/"),
+					},
+					{
+						UID:          tree2[1].UID,
+						Fullpath:     "tree2-folder-0/tree2-folder-1",
+						FullpathUIDs: strings.Join([]string{tree2[0].UID, tree2[1].UID}, "/"),
+					},
+					{
+						UID:          tree2[2].UID,
+						Fullpath:     "tree2-folder-0/tree2-folder-1/tree2-folder-2",
+						FullpathUIDs: strings.Join([]string{tree2[0].UID, tree2[1].UID, tree2[2].UID}, "/"),
+					},
+				},
+			},
+		}
+
+		for _, tc := range testCases {
+			t.Run(tc.name, func(t *testing.T) {
+				actualFolders, err := serviceWithFlagOn.GetFolders(context.Background(), tc.cmd)
+				require.NoError(t, err)
+
+				require.NoError(t, err)
+				require.Len(t, actualFolders, len(tc.expected))
+
+				for i, expected := range tc.expected {
+					actualFolder := actualFolders[i]
+					require.Equal(t, expected.UID, actualFolder.UID)
+					if tc.cmd.WithFullpath {
+						require.Equal(t, expected.Fullpath, actualFolder.Fullpath)
+					} else {
+						require.Empty(t, actualFolder.Fullpath)
+					}
+
+					if tc.cmd.WithFullpathUIDs {
+						require.Equal(t, expected.FullpathUIDs, actualFolder.FullpathUIDs)
+					} else {
+						require.Empty(t, actualFolder.FullpathUIDs)
+					}
+				}
+			})
+		}
+	})
 }
 
-func CreateSubtreeInStore(t *testing.T, store *sqlStore, service *Service, depth int, prefix string, cmd folder.CreateFolderCommand) []string {
+func CreateSubtreeInStore(t *testing.T, store *sqlStore, service *Service, depth int, prefix string, cmd folder.CreateFolderCommand) []*folder.Folder {
 	t.Helper()
 
-	ancestorUIDs := []string{}
-	if cmd.ParentUID != "" {
-		ancestorUIDs = append(ancestorUIDs, cmd.ParentUID)
-	}
+	folders := make([]*folder.Folder, 0, depth)
 	for i := 0; i < depth; i++ {
 		title := fmt.Sprintf("%sfolder-%d", prefix, i)
 		cmd.Title = title
@@ -1422,23 +1631,12 @@ func CreateSubtreeInStore(t *testing.T, store *sqlStore, service *Service, depth
 		require.Equal(t, title, f.Title)
 		require.NotEmpty(t, f.UID)
 
-		parents, err := store.GetParents(context.Background(), folder.GetParentsQuery{
-			UID:   f.UID,
-			OrgID: cmd.OrgID,
-		})
-		require.NoError(t, err)
-		parentUIDs := []string{}
-		for _, p := range parents {
-			parentUIDs = append(parentUIDs, p.UID)
-		}
-		require.Equal(t, ancestorUIDs, parentUIDs)
-
-		ancestorUIDs = append(ancestorUIDs, f.UID)
+		folders = append(folders, f)
 
 		cmd.ParentUID = f.UID
 	}
 
-	return ancestorUIDs
+	return folders
 }
 
 func setup(t *testing.T, dashStore dashboards.Store, dashboardFolderStore folder.FolderStore, nestedFolderStore store, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl, db db.DB) folder.Service {
diff --git a/pkg/services/folder/folderimpl/sqlstore.go b/pkg/services/folder/folderimpl/sqlstore.go
index c7e8033bff7..b107222bc59 100644
--- a/pkg/services/folder/folderimpl/sqlstore.go
+++ b/pkg/services/folder/folderimpl/sqlstore.go
@@ -2,6 +2,7 @@ package folderimpl
 
 import (
 	"context"
+	"fmt"
 	"runtime"
 	"strings"
 	"time"
@@ -12,10 +13,13 @@ import (
 	"github.com/grafana/grafana/pkg/infra/log"
 	"github.com/grafana/grafana/pkg/services/dashboards"
 	"github.com/grafana/grafana/pkg/services/folder"
+	"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/util"
 )
 
+const DEFAULT_BATCH_SIZE = 999
+
 type sqlStore struct {
 	db  db.DB
 	log log.Logger
@@ -342,27 +346,176 @@ func (ss *sqlStore) GetHeight(ctx context.Context, foldrUID string, orgID int64,
 	return height, nil
 }
 
-func (ss *sqlStore) GetFolders(ctx context.Context, orgID int64, uids []string) ([]*folder.Folder, error) {
-	if len(uids) == 0 {
-		return []*folder.Folder{}, nil
+// GetFolders returns org folders by their UIDs.
+// If UIDs is empty, it returns all folders in the org.
+// If WithFullpath is true it computes also the full path of a folder.
+// The full path is a string that contains the titles of all parent folders separated by a slash.
+// For example, if the folder structure is:
+//
+//	A
+//	└── B
+//	    └── C
+//
+// The full path of C is "A/B/C".
+// The full path of B is "A/B".
+// The full path of A is "A".
+// If a folder contains a slash in its title, it is escaped with a backslash.
+// For example, if the folder structure is:
+//
+//	A
+//	└── B/C
+//
+// The full path of C is "A/B\/C".
+//
+// If FullpathUIDs is true it computes a string that contains the UIDs of all parent folders separated by slash.
+// For example, if the folder structure is:
+//
+//	A (uid: "uid1")
+//	└── B (uid: "uid2")
+//	    └── C (uid: "uid3")
+//
+// The full path UIDs of C is "uid1/uid2/uid3".
+// The full path UIDs of B is "uid1/uid2".
+// The full path UIDs of A is "uid1".
+func (ss *sqlStore) GetFolders(ctx context.Context, q getFoldersQuery) ([]*folder.Folder, error) {
+	if q.BatchSize == 0 {
+		q.BatchSize = DEFAULT_BATCH_SIZE
 	}
+
 	var folders []*folder.Folder
 	if err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
-		b := strings.Builder{}
-		b.WriteString(`SELECT * FROM folder WHERE org_id=? AND uid IN (?` + strings.Repeat(", ?", len(uids)-1) + `)`)
-		args := []any{orgID}
-		for _, uid := range uids {
-			args = append(args, uid)
-		}
-		return sess.SQL(b.String(), args...).Find(&folders)
+		return batch(len(q.UIDs), int(q.BatchSize), func(start, end int) error {
+			partialFolders := make([]*folder.Folder, 0, q.BatchSize)
+			partialUIDs := q.UIDs[start:min(end, len(q.UIDs))]
+			s := strings.Builder{}
+			s.WriteString(`SELECT f0.id, f0.org_id, f0.uid, f0.parent_uid, f0.title, f0.description, f0.created, f0.updated`)
+			// compute full path column if requested
+			if q.WithFullpath {
+				s.WriteString(fmt.Sprintf(`, %s AS fullpath`, getFullpathSQL(ss.db.GetDialect())))
+			}
+			// compute full path UIDs column if requested
+			if q.WithFullpathUIDs {
+				s.WriteString(fmt.Sprintf(`, %s AS fullpath_uids`, getFullapathUIDsSQL(ss.db.GetDialect())))
+			}
+			s.WriteString(` FROM folder f0`)
+			// join the same table multiple times to compute the full path of a folder
+			if q.WithFullpath || q.WithFullpathUIDs || len(q.ancestorUIDs) > 0 {
+				s.WriteString(getFullpathJoinsSQL())
+			}
+			s.WriteString(` WHERE f0.org_id=?`)
+			args := []any{q.OrgID}
+			if len(partialUIDs) > 0 {
+				s.WriteString(` AND f0.uid IN (?` + strings.Repeat(", ?", len(partialUIDs)-1) + `)`)
+				for _, uid := range partialUIDs {
+					args = append(args, uid)
+				}
+			}
+
+			if len(q.ancestorUIDs) == 0 {
+				err := sess.SQL(s.String(), args...).Find(&partialFolders)
+				if err != nil {
+					return err
+				}
+				folders = append(folders, partialFolders...)
+				return nil
+			}
+
+			// filter out folders if they are not in the subtree of the given ancestor folders
+			if err := batch(len(q.ancestorUIDs), int(q.BatchSize), func(start2, end2 int) error {
+				s2, args2 := getAncestorsSQL(ss.db.GetDialect(), q.ancestorUIDs, start2, end2, s.String(), args)
+				err := sess.SQL(s2, args2...).Find(&partialFolders)
+				if err != nil {
+					return err
+				}
+				folders = append(folders, partialFolders...)
+				return nil
+			}); err != nil {
+				return err
+			}
+			return nil
+		})
 	}); err != nil {
 		return nil, err
 	}
 
 	// Add URLs
 	for i, f := range folders {
+		f.Fullpath = strings.TrimLeft(f.Fullpath, "/")
+		f.FullpathUIDs = strings.TrimLeft(f.FullpathUIDs, "/")
 		folders[i] = f.WithURL()
 	}
 
 	return folders, nil
 }
+
+func getFullpathSQL(dialect migrator.Dialect) string {
+	concatCols := make([]string, 0, folder.MaxNestedFolderDepth)
+	concatCols = append(concatCols, "COALESCE(REPLACE(f0.title, '/', '\\/'), '')")
+	for i := 1; i <= folder.MaxNestedFolderDepth; i++ {
+		concatCols = append([]string{fmt.Sprintf("COALESCE(REPLACE(f%d.title, '/', '\\/'), '')", i), "'/'"}, concatCols...)
+	}
+	return dialect.Concat(concatCols...)
+}
+
+func getFullapathUIDsSQL(dialect migrator.Dialect) string {
+	concatCols := make([]string, 0, folder.MaxNestedFolderDepth)
+	concatCols = append(concatCols, "COALESCE(f0.uid, '')")
+	for i := 1; i <= folder.MaxNestedFolderDepth; i++ {
+		concatCols = append([]string{fmt.Sprintf("COALESCE(f%d.uid, '')", i), "'/'"}, concatCols...)
+	}
+	return dialect.Concat(concatCols...)
+}
+
+// getFullpathJoinsSQL returns a SQL fragment that joins the same table multiple times to get the full path of a folder.
+func getFullpathJoinsSQL() string {
+	joins := make([]string, 0, folder.MaxNestedFolderDepth)
+	for i := 1; i <= folder.MaxNestedFolderDepth; i++ {
+		joins = append(joins, fmt.Sprintf(` LEFT JOIN folder f%d ON f%d.org_id = f%d.org_id AND f%d.uid = f%d.parent_uid`, i, i, i-1, i, i-1))
+	}
+	return strings.Join(joins, "\n")
+}
+
+func getAncestorsSQL(dialect migrator.Dialect, ancestorUIDs []string, start int, end int, origSQL string, origArgs []any) (string, []any) {
+	s2 := strings.Builder{}
+	s2.WriteString(origSQL)
+	args2 := make([]any, 0, len(ancestorUIDs)*folder.MaxNestedFolderDepth)
+	args2 = append(args2, origArgs...)
+
+	partialAncestorUIDs := ancestorUIDs[start:min(end, len(ancestorUIDs))]
+	partialArgs := make([]any, 0, len(partialAncestorUIDs))
+	for _, uid := range partialAncestorUIDs {
+		partialArgs = append(partialArgs, uid)
+	}
+	s2.WriteString(` AND ( f0.uid IN (?` + strings.Repeat(", ?", len(partialAncestorUIDs)-1) + `)`)
+	args2 = append(args2, partialArgs...)
+	for i := 1; i <= folder.MaxNestedFolderDepth; i++ {
+		s2.WriteString(fmt.Sprintf(` OR f%d.uid IN (?`+strings.Repeat(", ?", len(partialAncestorUIDs)-1)+`)`, i))
+		args2 = append(args2, partialArgs...)
+	}
+	s2.WriteString(` )`)
+	return s2.String(), args2
+}
+
+func batch(count, batchSize int, eachFn func(start, end int) error) error {
+	if count == 0 {
+		if err := eachFn(0, 0); err != nil {
+			return err
+		}
+		return nil
+	}
+
+	for i := 0; i < count; {
+		end := i + batchSize
+		if end > count {
+			end = count
+		}
+
+		if err := eachFn(i, end); err != nil {
+			return err
+		}
+
+		i = end
+	}
+
+	return nil
+}
diff --git a/pkg/services/folder/folderimpl/sqlstore_test.go b/pkg/services/folder/folderimpl/sqlstore_test.go
index 6d2151aec80..7729c8fbab6 100644
--- a/pkg/services/folder/folderimpl/sqlstore_test.go
+++ b/pkg/services/folder/folderimpl/sqlstore_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
+	"github.com/google/uuid"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
@@ -759,24 +760,91 @@ func TestIntegrationGetFolders(t *testing.T) {
 	})
 
 	t.Run("get folders by UIDs should succeed", func(t *testing.T) {
-		ff, err := folderStore.GetFolders(context.Background(), orgID, uids)
+		actualFolders, err := folderStore.GetFolders(context.Background(), NewGetFoldersQuery(folder.GetFoldersQuery{OrgID: orgID, UIDs: uids[1:]}))
 		require.NoError(t, err)
-		assert.Equal(t, len(uids), len(ff))
-		for _, f := range folders {
-			folderInResponseIdx := slices.IndexFunc(ff, func(rf *folder.Folder) bool {
+		assert.Equal(t, len(uids[1:]), len(actualFolders))
+		for _, f := range folders[1:] {
+			folderInResponseIdx := slices.IndexFunc(actualFolders, func(rf *folder.Folder) bool {
 				return rf.UID == f.UID
 			})
 			assert.NotEqual(t, -1, folderInResponseIdx)
-			rf := ff[folderInResponseIdx]
-			assert.Equal(t, f.UID, rf.UID)
-			assert.Equal(t, f.OrgID, rf.OrgID)
-			assert.Equal(t, f.Title, rf.Title)
-			assert.Equal(t, f.Description, rf.Description)
-			assert.NotEmpty(t, rf.Created)
-			assert.NotEmpty(t, rf.Updated)
-			assert.NotEmpty(t, rf.URL)
+			actualFolder := actualFolders[folderInResponseIdx]
+			assert.Equal(t, f.UID, actualFolder.UID)
+			assert.Equal(t, f.OrgID, actualFolder.OrgID)
+			assert.Equal(t, f.Title, actualFolder.Title)
+			assert.Equal(t, f.Description, actualFolder.Description)
+			assert.NotEmpty(t, actualFolder.Created)
+			assert.NotEmpty(t, actualFolder.Updated)
+			assert.NotEmpty(t, actualFolder.URL)
 		}
 	})
+
+	t.Run("get folders by UIDs batching should work as expected", func(t *testing.T) {
+		q := NewGetFoldersQuery(folder.GetFoldersQuery{OrgID: orgID, UIDs: uids[1:], BatchSize: 3})
+		actualFolders, err := folderStore.GetFolders(context.Background(), q)
+		require.NoError(t, err)
+		assert.Equal(t, len(uids[1:]), len(actualFolders))
+		for _, f := range folders[1:] {
+			folderInResponseIdx := slices.IndexFunc(actualFolders, func(rf *folder.Folder) bool {
+				return rf.UID == f.UID
+			})
+			assert.NotEqual(t, -1, folderInResponseIdx)
+			actualFolder := actualFolders[folderInResponseIdx]
+			assert.Equal(t, f.UID, actualFolder.UID)
+			assert.Equal(t, f.OrgID, actualFolder.OrgID)
+			assert.Equal(t, f.Title, actualFolder.Title)
+			assert.Equal(t, f.Description, actualFolder.Description)
+			assert.NotEmpty(t, actualFolder.Created)
+			assert.NotEmpty(t, actualFolder.Updated)
+			assert.NotEmpty(t, actualFolder.URL)
+		}
+	})
+
+	t.Run("get folders by UIDs with fullpath should succeed", func(t *testing.T) {
+		q := NewGetFoldersQuery(folder.GetFoldersQuery{OrgID: orgID, UIDs: uids[1:], WithFullpath: true})
+		q.BatchSize = 3
+		actualFolders, err := folderStore.GetFolders(context.Background(), q)
+		require.NoError(t, err)
+		assert.Equal(t, len(uids[1:]), len(actualFolders))
+		for _, f := range folders[1:] {
+			folderInResponseIdx := slices.IndexFunc(actualFolders, func(rf *folder.Folder) bool {
+				return rf.UID == f.UID
+			})
+			assert.NotEqual(t, -1, folderInResponseIdx)
+			actualFolder := actualFolders[folderInResponseIdx]
+			assert.Equal(t, f.UID, actualFolder.UID)
+			assert.Equal(t, f.OrgID, actualFolder.OrgID)
+			assert.Equal(t, f.Title, actualFolder.Title)
+			assert.Equal(t, f.Description, actualFolder.Description)
+			assert.NotEmpty(t, actualFolder.Created)
+			assert.NotEmpty(t, actualFolder.Updated)
+			assert.NotEmpty(t, actualFolder.URL)
+			assert.NotEmpty(t, actualFolder.Fullpath)
+		}
+	})
+
+	t.Run("get folders by UIDs and ancestor UIDs should work as expected", func(t *testing.T) {
+		q := NewGetFoldersQuery(folder.GetFoldersQuery{OrgID: orgID, UIDs: uids[1:], BatchSize: 3})
+		q.ancestorUIDs = make([]string, 0, int(q.BatchSize)+1)
+		for i := 0; i < int(q.BatchSize); i++ {
+			q.ancestorUIDs = append(q.ancestorUIDs, uuid.New().String())
+		}
+		q.ancestorUIDs = append(q.ancestorUIDs, folders[len(folders)-1].UID)
+
+		actualFolders, err := folderStore.GetFolders(context.Background(), q)
+		require.NoError(t, err)
+		assert.Equal(t, 1, len(actualFolders))
+
+		f := folders[len(folders)-1]
+		actualFolder := actualFolders[0]
+		assert.Equal(t, f.UID, actualFolder.UID)
+		assert.Equal(t, f.OrgID, actualFolder.OrgID)
+		assert.Equal(t, f.Title, actualFolder.Title)
+		assert.Equal(t, f.Description, actualFolder.Description)
+		assert.NotEmpty(t, actualFolder.Created)
+		assert.NotEmpty(t, actualFolder.Updated)
+		assert.NotEmpty(t, actualFolder.URL)
+	})
 }
 
 func CreateOrg(t *testing.T, db *sqlstore.SQLStore) int64 {
diff --git a/pkg/services/folder/folderimpl/store.go b/pkg/services/folder/folderimpl/store.go
index 4472593c268..4cee3fcf162 100644
--- a/pkg/services/folder/folderimpl/store.go
+++ b/pkg/services/folder/folderimpl/store.go
@@ -6,6 +6,18 @@ import (
 	"github.com/grafana/grafana/pkg/services/folder"
 )
 
+type getFoldersQuery struct {
+	folder.GetFoldersQuery
+	ancestorUIDs []string
+}
+
+func NewGetFoldersQuery(q folder.GetFoldersQuery) getFoldersQuery {
+	return getFoldersQuery{
+		GetFoldersQuery: q,
+		ancestorUIDs:    []string{},
+	}
+}
+
 // store is the interface which a folder store must implement.
 type store interface {
 	// Create creates a folder and returns the newly-created folder.
@@ -35,5 +47,5 @@ type store interface {
 	GetHeight(ctx context.Context, foldrUID string, orgID int64, parentUID *string) (int, error)
 
 	// GetFolders returns folders with given uids
-	GetFolders(ctx context.Context, orgID int64, uids []string) ([]*folder.Folder, error)
+	GetFolders(ctx context.Context, q getFoldersQuery) ([]*folder.Folder, error)
 }
diff --git a/pkg/services/folder/folderimpl/store_fake.go b/pkg/services/folder/folderimpl/store_fake.go
index c373d44674c..f355bd17262 100644
--- a/pkg/services/folder/folderimpl/store_fake.go
+++ b/pkg/services/folder/folderimpl/store_fake.go
@@ -57,6 +57,6 @@ func (f *fakeStore) GetHeight(ctx context.Context, folderUID string, orgID int64
 	return f.ExpectedFolderHeight, f.ExpectedError
 }
 
-func (f *fakeStore) GetFolders(ctx context.Context, orgID int64, uids []string) ([]*folder.Folder, error) {
+func (f *fakeStore) GetFolders(ctx context.Context, q getFoldersQuery) ([]*folder.Folder, error) {
 	return f.ExpectedFolders, f.ExpectedError
 }
diff --git a/pkg/services/folder/foldertest/foldertest.go b/pkg/services/folder/foldertest/foldertest.go
index 8a0864985b4..7cfccb3b5c2 100644
--- a/pkg/services/folder/foldertest/foldertest.go
+++ b/pkg/services/folder/foldertest/foldertest.go
@@ -51,3 +51,7 @@ func (s *FakeService) RegisterService(service folder.RegistryService) error {
 func (s *FakeService) GetDescendantCounts(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) {
 	return s.ExpectedDescendantCounts, s.ExpectedError
 }
+
+func (s *FakeService) GetFolders(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) {
+	return s.ExpectedFolders, s.ExpectedError
+}
diff --git a/pkg/services/folder/model.go b/pkg/services/folder/model.go
index a28f24fe8f4..da7fa6c837a 100644
--- a/pkg/services/folder/model.go
+++ b/pkg/services/folder/model.go
@@ -41,11 +41,13 @@ type Folder struct {
 
 	// TODO: validate if this field is required/relevant to folders.
 	// currently there is no such column
-	Version   int
-	URL       string
-	UpdatedBy int64
-	CreatedBy int64
-	HasACL    bool
+	Version      int
+	URL          string
+	UpdatedBy    int64
+	CreatedBy    int64
+	HasACL       bool
+	Fullpath     string `xorm:"fullpath"`
+	FullpathUIDs string `xorm:"fullpath_uids"`
 }
 
 var GeneralFolder = Folder{ID: 0, Title: "General"}
@@ -149,6 +151,16 @@ type GetFolderQuery struct {
 	SignedInUser identity.Requester `json:"-"`
 }
 
+type GetFoldersQuery struct {
+	OrgID            int64
+	UIDs             []string
+	WithFullpath     bool
+	WithFullpathUIDs bool
+	BatchSize        uint64
+
+	SignedInUser identity.Requester `json:"-"`
+}
+
 // GetParentsQuery captures the information required by the folder service to
 // return a list of all parent folders of a given folder.
 type GetParentsQuery struct {
diff --git a/pkg/services/folder/service.go b/pkg/services/folder/service.go
index 7bca2417a9d..242a00e2f0b 100644
--- a/pkg/services/folder/service.go
+++ b/pkg/services/folder/service.go
@@ -25,6 +25,12 @@ type Service interface {
 	// Move changes a folder's parent folder to the requested new parent.
 	Move(ctx context.Context, cmd *MoveFolderCommand) (*Folder, error)
 	RegisterService(service RegistryService) error
+	// GetFolders returns org folders that are accessible by the signed in user by their UIDs.
+	// If WithFullpath is true it computes also the full path of a folder.
+	// The full path is a string that contains the titles of all parent folders separated by a slash.
+	// If a folder contains a slash in its title, it is escaped with a backslash.
+	// If FullpathUIDs is true it computes a string that contains the UIDs of all parent folders separated by slash.
+	GetFolders(ctx context.Context, q GetFoldersQuery) ([]*Folder, error)
 	GetDescendantCounts(ctx context.Context, q *GetDescendantCountsQuery) (DescendantCounts, error)
 }
 
diff --git a/pkg/services/sqlstore/migrator/dialect.go b/pkg/services/sqlstore/migrator/dialect.go
index 53b75365a20..5aae742b6ca 100644
--- a/pkg/services/sqlstore/migrator/dialect.go
+++ b/pkg/services/sqlstore/migrator/dialect.go
@@ -91,6 +91,10 @@ type Dialect interface {
 	// column names to values to use in the where clause.
 	// The update is executed as part of the provided session.
 	Update(ctx context.Context, tx *session.SessionTx, tableName string, row map[string]any, where map[string]any) error
+	// Concat returns the sql statement for concating multiple strings
+	// Implementations are not expected to quote the arguments
+	// therefore any callers should take care to quote arguments as necessary
+	Concat(...string) string
 }
 
 type LockCfg struct {
@@ -450,3 +454,7 @@ func (b *BaseDialect) Update(ctx context.Context, tx *session.SessionTx, tableNa
 	_, err = tx.Exec(ctx, query, args...)
 	return err
 }
+
+func (b *BaseDialect) Concat(strs ...string) string {
+	return fmt.Sprintf("CONCAT(%s)", strings.Join(strs, ", "))
+}
diff --git a/pkg/services/sqlstore/migrator/sqlite_dialect.go b/pkg/services/sqlstore/migrator/sqlite_dialect.go
index 17bb097e199..c2979ea1925 100644
--- a/pkg/services/sqlstore/migrator/sqlite_dialect.go
+++ b/pkg/services/sqlstore/migrator/sqlite_dialect.go
@@ -209,3 +209,7 @@ func (db *SQLite3) UpsertMultipleSQL(tableName string, keyCols, updateCols []str
 	)
 	return s, nil
 }
+
+func (db *SQLite3) Concat(strs ...string) string {
+	return strings.Join(strs, " || ")
+}