diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md
index e1d9930563586b55eb16e5a89e173bbcc1f58b4d..d7d5608aa085c0ea5c1c8d401ad26a6484976ca9 100644
--- a/docs/usage/self-hosted-configuration.md
+++ b/docs/usage/self-hosted-configuration.md
@@ -125,6 +125,20 @@ If using negations, all repositories except those who match the regex are added
 }
 ```
 
+## autodiscoverNamespaces
+
+You can use this option to autodiscover projects in specific namespaces (a.k.a. groups/organizations/workspaces).
+In contrast to `autodiscoverFilter` the filtering is done by the platform and therefore more efficient.
+
+For example:
+
+```json
+{
+  "platform": "gitlab",
+  "autodiscoverNamespaces": ["a-group", "another-group/some-subgroup"]
+}
+```
+
 ## autodiscoverTopics
 
 Some platforms allow you to add tags, or topics, to repositories and retrieve repository lists by specifying those
diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts
index a1c4eb3c8edde105230d7c80130c56a2e5a9db07..ae283616777b3d28ca27dd669bb00204504258c7 100644
--- a/lib/config/options/index.ts
+++ b/lib/config/options/index.ts
@@ -797,6 +797,17 @@ const options: RenovateOptions[] = [
     default: null,
     globalOnly: true,
   },
+  {
+    name: 'autodiscoverNamespaces',
+    description:
+      'Filter the list of autodiscovered repositories by namespaces.',
+    stage: 'global',
+    type: 'array',
+    subType: 'string',
+    default: null,
+    globalOnly: true,
+    supportedPlatforms: ['gitlab'],
+  },
   {
     name: 'autodiscoverTopics',
     description: 'Filter the list of autodiscovered repositories by topics.',
diff --git a/lib/config/types.ts b/lib/config/types.ts
index 1f2ac991c4f7be3aee1545357cbd36baa2d403bc..e849702b44b66d5b884f8fecfd1bc8297170439a 100644
--- a/lib/config/types.ts
+++ b/lib/config/types.ts
@@ -95,6 +95,7 @@ export interface RenovateSharedConfig {
 export interface GlobalOnlyConfig {
   autodiscover?: boolean;
   autodiscoverFilter?: string[] | string;
+  autodiscoverNamespaces?: string[];
   autodiscoverTopics?: string[];
   baseDir?: string;
   cacheDir?: string;
diff --git a/lib/modules/platform/gitlab/index.spec.ts b/lib/modules/platform/gitlab/index.spec.ts
index f6f9e32203e09bb83e2088d7190be4a6c4844d28..cfa104946eec7c4c8a62478628316afeee354e61 100644
--- a/lib/modules/platform/gitlab/index.spec.ts
+++ b/lib/modules/platform/gitlab/index.spec.ts
@@ -209,6 +209,53 @@ describe('modules/platform/gitlab/index', () => {
       const repos = await gitlab.getRepos({ topics: ['one', 'two'] });
       expect(repos).toEqual(['a/b', 'c/d']);
     });
+
+    it('should query the groups endpoint for each namespace', async () => {
+      httpMock
+        .scope(gitlabApiHost)
+        .get(
+          '/api/v4/groups/a/projects?membership=true&per_page=100&with_merge_requests_enabled=true&min_access_level=30&archived=false&include_subgroups=true&with_shared=false'
+        )
+        .reply(200, [
+          {
+            path_with_namespace: 'a/b',
+          },
+        ])
+        .get(
+          '/api/v4/groups/c%2Fd/projects?membership=true&per_page=100&with_merge_requests_enabled=true&min_access_level=30&archived=false&include_subgroups=true&with_shared=false'
+        )
+        .reply(200, [
+          {
+            path_with_namespace: 'c/d/e',
+          },
+          {
+            path_with_namespace: 'c/d/f',
+          },
+        ]);
+      const repos = await gitlab.getRepos({ namespaces: ['a', 'c/d'] });
+      expect(repos).toEqual(['a/b', 'c/d/e', 'c/d/f']);
+    });
+
+    it('should consider topics when querying the groups endpoint', async () => {
+      httpMock
+        .scope(gitlabApiHost)
+        .get(
+          '/api/v4/groups/a/projects?membership=true&per_page=100&with_merge_requests_enabled=true&min_access_level=30&archived=false&include_subgroups=true&with_shared=false&topic=one%2Ctwo'
+        )
+        .reply(200, [
+          {
+            path_with_namespace: 'a/b',
+          },
+          {
+            path_with_namespace: 'a/c',
+          },
+        ]);
+      const repos = await gitlab.getRepos({
+        namespaces: ['a'],
+        topics: ['one', 'two'],
+      });
+      expect(repos).toEqual(['a/b', 'a/c']);
+    });
   });
 
   async function initRepo(
diff --git a/lib/modules/platform/gitlab/index.ts b/lib/modules/platform/gitlab/index.ts
index d8981e5c72e64515c3c0339313105bdbac21025d..539fdb4dfa63d71727c5e7a6c33b98958b814e92 100644
--- a/lib/modules/platform/gitlab/index.ts
+++ b/lib/modules/platform/gitlab/index.ts
@@ -2,6 +2,7 @@ import URL from 'node:url';
 import { setTimeout } from 'timers/promises';
 import is from '@sindresorhus/is';
 import JSON5 from 'json5';
+import pMap from 'p-map';
 import semver from 'semver';
 import {
   CONFIG_GIT_URL_UNAVAILABLE,
@@ -162,14 +163,38 @@ export async function getRepos(config?: AutodiscoverConfig): Promise<string[]> {
     queryParams['topic'] = config.topics.join(',');
   }
 
-  const url = 'projects?' + getQueryString(queryParams);
+  const urls = [];
+  if (config?.namespaces?.length) {
+    queryParams['with_shared'] = false;
+    queryParams['include_subgroups'] = true;
+    urls.push(
+      ...config.namespaces.map(
+        (namespace) =>
+          `groups/${urlEscape(namespace)}/projects?${getQueryString(
+            queryParams
+          )}`
+      )
+    );
+  } else {
+    urls.push('projects?' + getQueryString(queryParams));
+  }
 
   try {
-    const res = await gitlabApi.getJson<RepoResponse[]>(url, {
-      paginate: true,
-    });
-    logger.debug(`Discovered ${res.body.length} project(s)`);
-    return res.body
+    const repos = (
+      await pMap(
+        urls,
+        (url) =>
+          gitlabApi.getJson<RepoResponse[]>(url, {
+            paginate: true,
+          }),
+        {
+          concurrency: 2,
+        }
+      )
+    ).flatMap((response) => response.body);
+
+    logger.debug(`Discovered ${repos.length} project(s)`);
+    return repos
       .filter((repo) => !repo.mirror || config?.includeMirrors)
       .map((repo) => repo.path_with_namespace);
   } catch (err) {
@@ -177,6 +202,7 @@ export async function getRepos(config?: AutodiscoverConfig): Promise<string[]> {
     throw err;
   }
 }
+
 function urlEscape(str: string): string;
 function urlEscape(str: string | undefined): string | undefined;
 function urlEscape(str: string | undefined): string | undefined {
diff --git a/lib/modules/platform/types.ts b/lib/modules/platform/types.ts
index 684005d9c944b8be42585dbb2c8849360e32a745..1e87dec950714856ad3dda5bcc5124be98f2b116 100644
--- a/lib/modules/platform/types.ts
+++ b/lib/modules/platform/types.ts
@@ -169,6 +169,7 @@ export type EnsureIssueResult = 'updated' | 'created';
 export interface AutodiscoverConfig {
   topics?: string[];
   includeMirrors?: boolean;
+  namespaces?: string[];
 }
 
 export interface Platform {
diff --git a/lib/workers/global/autodiscover.ts b/lib/workers/global/autodiscover.ts
index 5d1963ffaaa9b11e92f352f0f1ce38bc3985ff70..2af10cf07fc277eea9939383659aede8a4c7521e 100644
--- a/lib/workers/global/autodiscover.ts
+++ b/lib/workers/global/autodiscover.ts
@@ -38,6 +38,7 @@ export async function autodiscoverRepositories(
   let discovered = await platform.getRepos({
     topics: config.autodiscoverTopics,
     includeMirrors: config.includeMirrors,
+    namespaces: config.autodiscoverNamespaces,
   });
   if (!discovered?.length) {
     // Soft fail (no error thrown) if no accessible repositories