diff --git a/ee/app/assets/javascripts/groups/settings/work_items/custom_fields_list.vue b/ee/app/assets/javascripts/groups/settings/work_items/custom_fields_list.vue
new file mode 100644
index 0000000000000000000000000000000000000000..99c94e8ccde3adf272f1eddb9b5b981851b8caab
--- /dev/null
+++ b/ee/app/assets/javascripts/groups/settings/work_items/custom_fields_list.vue
@@ -0,0 +1,188 @@
+<script>
+import { GlBadge, GlButton, GlIntersperse, GlSprintf, GlTable } from '@gitlab/ui';
+import { humanize } from '~/lib/utils/text_utility';
+import { __, n__, s__ } from '~/locale';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import groupCustomFieldsQuery from './group_custom_fields.query.graphql';
+
+export default {
+  components: {
+    GlBadge,
+    GlButton,
+    GlIntersperse,
+    GlSprintf,
+    GlTable,
+    TimeagoTooltip,
+  },
+  inject: ['fullPath'],
+  data() {
+    return {
+      customFields: [],
+      customFieldsForList: [],
+    };
+  },
+  apollo: {
+    customFields: {
+      query: groupCustomFieldsQuery,
+      variables() {
+        return {
+          fullPath: this.fullPath,
+        };
+      },
+      update(data) {
+        return data.group.customFields;
+      },
+      result() {
+        // need a copy of the apollo query response as the table adds
+        // properties to it for showing the detail view
+        // prevents "Cannot add property _showDetails, object is not extensible" error
+        this.customFieldsForList = this.customFields?.nodes?.map((field) => ({ ...field })) ?? [];
+      },
+      error(error) {
+        Sentry.captureException(error.message);
+      },
+    },
+  },
+  methods: {
+    detailsToggleIcon(detailsVisible) {
+      return detailsVisible ? 'chevron-down' : 'chevron-right';
+    },
+    formattedFieldType(item) {
+      return humanize(item.fieldType.toLowerCase());
+    },
+    selectOptionsText(item) {
+      if (item.selectOptions.length > 0) {
+        return n__('%d option', '%d options', item.selectOptions.length);
+      }
+      return null;
+    },
+  },
+  fields: [
+    {
+      key: 'show_details',
+      label: '',
+      class: 'gl-w-0 !gl-align-middle',
+    },
+    {
+      key: 'name',
+      label: s__('WorkItem|Field'),
+      class: '!gl-align-middle',
+    },
+    {
+      key: 'fieldType',
+      label: s__('WorkItem|Type'),
+      class: '!gl-align-middle',
+    },
+    {
+      key: 'usage',
+      label: s__('WorkItem|Usage'),
+      class: '!gl-align-middle',
+    },
+    {
+      key: 'lastModified',
+      label: __('Last modified'),
+      class: '!gl-align-middle',
+    },
+    {
+      key: 'actions',
+      label: __('Actions'),
+      class: 'gl-w-0 gl-text-right',
+    },
+  ],
+};
+</script>
+
+<template>
+  <div>
+    <div class="gl-font-lg gl-border gl-rounded-t-base gl-border-b-0 gl-p-5 gl-font-bold">
+      {{ s__('WorkItem|Active custom fields') }}
+      <gl-badge v-if="!$apollo.queries.customFields.loading">
+        <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
+        {{ customFields.count }}/50
+      </gl-badge>
+    </div>
+    <gl-table
+      :items="customFieldsForList"
+      :fields="$options.fields"
+      outlined
+      responsive
+      class="gl-rounded-b-base !gl-bg-gray-10"
+    >
+      <template #cell(show_details)="row">
+        <gl-button
+          :aria-label="s__('WorkItem|Toggle details')"
+          :icon="detailsToggleIcon(row.detailsShowing)"
+          category="tertiary"
+          class="gl-align-self-flex-start"
+          data-testid="toggleDetailsButton"
+          @click="row.toggleDetails"
+        />
+      </template>
+      <template #cell(name)="{ item }">
+        {{ item.name }}
+      </template>
+      <template #cell(fieldType)="{ item }">
+        {{ formattedFieldType(item) }}
+        <div class="gl-text-secondary">{{ selectOptionsText(item) }}</div>
+      </template>
+      <template #cell(usage)="{ item }">
+        <gl-intersperse>
+          <span v-for="workItemType in item.workItemTypes" :key="workItemType.id">{{
+            workItemType.name
+          }}</span>
+        </gl-intersperse>
+      </template>
+      <template #cell(lastModified)="{ item }">
+        <timeago-tooltip :time="item.updatedAt" />
+      </template>
+      <template #cell(actions)>
+        <gl-button :aria-label="s__('WorkItem|Edit field')" icon="pencil" category="tertiary" />
+      </template>
+      <template #row-details="{ item }">
+        <div class="gl-border gl-col-span-5 gl-mt-3 gl-rounded-lg gl-bg-default gl-p-5">
+          <div class="gl-mb-3 gl-flex gl-gap-3">
+            <dt>{{ s__('WorkItem|Usage:') }}</dt>
+            <dd>
+              <gl-intersperse>
+                <span v-for="workItemType in item.workItemTypes" :key="workItemType.id">{{
+                  workItemType.name
+                }}</span>
+              </gl-intersperse>
+            </dd>
+          </div>
+          <div v-if="item.selectOptions.length > 0" class="gl-mb-3">
+            <dt>{{ s__('WorkItem|Options:') }}</dt>
+            <dd>
+              <ul>
+                <li v-for="option in item.selectOptions" :key="option.id">
+                  {{ option.value }}
+                </li>
+              </ul>
+            </dd>
+          </div>
+          <div class="gl-text-sm gl-text-secondary">
+            <gl-sprintf :message="s__('WorkItem|Last updated %{timeago}')">
+              <template #timeago>
+                <timeago-tooltip :time="item.updatedAt" />
+              </template>
+            </gl-sprintf>
+            &middot;
+            <gl-sprintf :message="s__('WorkItem|Created %{timeago}')">
+              <template #timeago>
+                <timeago-tooltip :time="item.updatedAt" />
+              </template>
+            </gl-sprintf>
+          </div>
+        </div>
+      </template>
+    </gl-table>
+  </div>
+</template>
+
+<style>
+/* remove border between row and details row */
+.gl-table tr.b-table-has-details td {
+  border-bottom-style: none;
+}
+</style>
diff --git a/ee/app/assets/javascripts/groups/settings/work_items/group_custom_fields.query.graphql b/ee/app/assets/javascripts/groups/settings/work_items/group_custom_fields.query.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..d5fb8a4385782b2e5a5560ec165e592f4a340cde
--- /dev/null
+++ b/ee/app/assets/javascripts/groups/settings/work_items/group_custom_fields.query.graphql
@@ -0,0 +1,24 @@
+query groupCustomFields($fullPath: ID!) {
+  group(fullPath: $fullPath) {
+    id
+    customFields(active: true) {
+      count
+      nodes {
+        id
+        name
+        fieldType
+        active
+        createdAt
+        updatedAt
+        selectOptions {
+          id
+          value
+        }
+        workItemTypes {
+          id
+          name
+        }
+      }
+    }
+  }
+}
diff --git a/ee/app/assets/javascripts/groups/settings/work_items/work_item_settings.js b/ee/app/assets/javascripts/groups/settings/work_items/work_item_settings.js
new file mode 100644
index 0000000000000000000000000000000000000000..5527eb8267abddc581582e2e5890f929ceda5b87
--- /dev/null
+++ b/ee/app/assets/javascripts/groups/settings/work_items/work_item_settings.js
@@ -0,0 +1,31 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import Translate from '~/vue_shared/translate';
+import CustomFieldsList from './custom_fields_list.vue';
+
+Vue.use(VueApollo);
+Vue.use(Translate);
+
+const apolloProvider = new VueApollo({
+  defaultClient: createDefaultClient(),
+});
+
+export function initWorkItemSettingsApp() {
+  const el = document.querySelector('#js-work-items-settings-form');
+  if (!el) return;
+
+  const { fullPath } = el.dataset;
+
+  // eslint-disable-next-line no-new
+  new Vue({
+    el,
+    apolloProvider,
+    provide: {
+      fullPath,
+    },
+    render(createElement) {
+      return createElement(CustomFieldsList);
+    },
+  });
+}
diff --git a/ee/app/assets/javascripts/pages/groups/settings/work_items/show/index.js b/ee/app/assets/javascripts/pages/groups/settings/work_items/show/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..b0d60d70123463dfa93d91a159c01ca3bd1213e6
--- /dev/null
+++ b/ee/app/assets/javascripts/pages/groups/settings/work_items/show/index.js
@@ -0,0 +1,3 @@
+import { initWorkItemSettingsApp } from 'ee/groups/settings/work_items/work_item_settings';
+
+initWorkItemSettingsApp();
diff --git a/ee/app/controllers/groups/settings/work_items_controller.rb b/ee/app/controllers/groups/settings/work_items_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0e0430edea497b20f2d2c9e1c02595db14f4cf35
--- /dev/null
+++ b/ee/app/controllers/groups/settings/work_items_controller.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Groups
+  module Settings
+    class WorkItemsController < Groups::ApplicationController
+      layout 'group_settings'
+
+      before_action :check_feature_availability
+
+      feature_category :team_planning
+      urgency :low
+
+      def show; end
+
+      private
+
+      def check_feature_availability
+        render_404 unless group.licensed_feature_available?(:custom_fields) &&
+          Feature.enabled?('custom_fields_feature', group)
+      end
+    end
+  end
+end
diff --git a/ee/app/views/groups/settings/work_items/show.html.haml b/ee/app/views/groups/settings/work_items/show.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..1a4c75ab45faca7e53689c259c05e9d4f0da42cf
--- /dev/null
+++ b/ee/app/views/groups/settings/work_items/show.html.haml
@@ -0,0 +1,16 @@
+- title = s_("WorkItem|Issues settings")
+- breadcrumb_title title
+- page_title title
+
+%h1.gl-heading-1.gl-mb-1.settings-title
+  = _('Issues')
+%p.gl-text-secondary
+  = s_('WorkItem|Configure work items such as epics, issues, and tasks to represent how your team works.')
+
+%h2.gl-heading-3.gl-mb-1.settings-title
+  = s_('WorkItem|Custom fields')
+%p.gl-text-secondary.gl-mb-3
+  = s_('WorkItem|Custom fields extend work items to track additional data. Fields will appear in alphanumeric order. All fields apply to all subgroups and projects.')
+  = link_to s_('WorkItem|How do I use custom fields?')
+
+#js-work-items-settings-form{ data: { full_path: @group.full_path } }
diff --git a/ee/config/routes/group.rb b/ee/config/routes/group.rb
index 9e9b57caebf197d05c84b4ce1febc657268e0bc7..1f3056e953c6fa5d73b28d2dc21d5f5070b8c580 100644
--- a/ee/config/routes/group.rb
+++ b/ee/config/routes/group.rb
@@ -31,6 +31,8 @@
       scope module: 'remote_development' do
         get 'workspaces', action: :show, controller: 'workspaces'
       end
+
+      resource :issues, only: [:show], controller: 'work_items'
     end
 
     resource :early_access_opt_in, only: %i[create show], controller: 'early_access_opt_in'
diff --git a/ee/spec/controllers/groups/settings/work_items_controller_spec.rb b/ee/spec/controllers/groups/settings/work_items_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..72b533ce0256bbc0d0836dc8428d1cdd340de6af
--- /dev/null
+++ b/ee/spec/controllers/groups/settings/work_items_controller_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::Settings::WorkItemsController, type: :controller, feature_category: :team_planning do
+  let(:group) { create(:group) }
+  let(:user) { create(:user) }
+
+  before do
+    sign_in(user)
+  end
+
+  describe 'GET #show' do
+    subject(:request) { get :show, params: { group_id: group.to_param } }
+
+    context 'when user is not authorized' do
+      it 'returns 404' do
+        request
+
+        expect(response).to have_gitlab_http_status(:not_found)
+      end
+    end
+
+    context 'when user is authorized' do
+      before do
+        group.add_owner(user)
+      end
+
+      context 'when custom_fields_feature is not available' do
+        before do
+          stub_licensed_features(custom_fields: false)
+        end
+
+        it 'returns 404' do
+          request
+
+          expect(response).to have_gitlab_http_status(:not_found)
+        end
+      end
+
+      context 'when custom_fields_feature is available' do
+        before do
+          stub_licensed_features(custom_fields: true)
+        end
+
+        context 'when custom_fields_feature flag is disabled' do
+          before do
+            stub_feature_flags(custom_fields_feature: false)
+          end
+
+          it 'returns 404' do
+            request
+
+            expect(response).to have_gitlab_http_status(:not_found)
+          end
+        end
+
+        context 'when custom_fields_feature flag is enabled' do
+          before do
+            stub_feature_flags(custom_fields_feature: true)
+          end
+
+          it 'renders the show template' do
+            request
+
+            expect(response).to have_gitlab_http_status(:ok)
+            expect(response).to render_template(:show)
+          end
+
+          it 'uses the group_settings layout' do
+            request
+
+            expect(response).to render_template('layouts/group_settings')
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/ee/spec/frontend/groups/settings/work_items/custom_fields_list_spec.js b/ee/spec/frontend/groups/settings/work_items/custom_fields_list_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b024f92eac3dcd9260bee9134a447df893651f63
--- /dev/null
+++ b/ee/spec/frontend/groups/settings/work_items/custom_fields_list_spec.js
@@ -0,0 +1,134 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { mount } from '@vue/test-utils';
+import { GlBadge } from '@gitlab/ui';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import CustomFieldsTable from 'ee/groups/settings/work_items/custom_fields_list.vue';
+import groupCustomFieldsQuery from 'ee/groups/settings/work_items/group_custom_fields.query.graphql';
+
+Vue.use(VueApollo);
+
+describe('CustomFieldsTable', () => {
+  let wrapper;
+
+  const findDetailsButton = () => wrapper.find('[data-testid="toggleDetailsButton"');
+
+  const selectField = {
+    id: '1',
+    name: 'Test Select Field',
+    fieldType: 'SELECT',
+    active: true,
+    workItemTypes: [{ id: '1', name: 'Issue' }],
+    selectOptions: [
+      {
+        id: 'select-1',
+        value: 'value 1',
+      },
+      {
+        id: 'select-2',
+        value: 'value 2',
+      },
+    ],
+    updatedAt: '2023-01-01T00:00:00Z',
+    createdAt: '2023-01-01T00:00:00Z',
+  };
+
+  const stringField = {
+    id: '1',
+    name: 'Test Text Field',
+    fieldType: 'STRING',
+    active: true,
+    workItemTypes: [
+      { id: '1', name: 'Issue' },
+      { id: '2', name: 'Task' },
+    ],
+    selectOptions: [],
+    updatedAt: '2023-01-01T00:00:00Z',
+    createdAt: '2023-01-01T00:00:00Z',
+  };
+
+  const createComponent = (fields = [selectField]) => {
+    const customFieldsResponse = jest.fn().mockResolvedValue({
+      data: {
+        group: {
+          id: '123',
+          customFields: {
+            nodes: fields,
+            count: fields.length,
+          },
+        },
+      },
+    });
+
+    wrapper = mount(CustomFieldsTable, {
+      provide: {
+        fullPath: 'group/path',
+      },
+      apolloProvider: createMockApollo([[groupCustomFieldsQuery, customFieldsResponse]]),
+      stubs: { GlIntersperse: true },
+    });
+  };
+
+  it('displays correct count in badge', async () => {
+    createComponent();
+    await waitForPromises();
+
+    const badge = wrapper.findComponent(GlBadge);
+    expect(badge.text()).toBe('1/50');
+  });
+
+  it('detailsToggleIcon returns correct icon based on visibility', async () => {
+    createComponent();
+    await waitForPromises();
+
+    expect(findDetailsButton().props().icon).toBe('chevron-right');
+
+    findDetailsButton().trigger('click');
+    await nextTick();
+
+    expect(findDetailsButton().props().icon).toBe('chevron-down');
+  });
+
+  it('humanizes field type', async () => {
+    createComponent();
+    await waitForPromises();
+
+    expect(wrapper.text()).toContain('Select');
+  });
+
+  it('selectOptionsText returns correct text based on options length', async () => {
+    createComponent();
+    await waitForPromises();
+
+    expect(wrapper.text()).toContain('2 options');
+  });
+
+  it('lists work item types', async () => {
+    createComponent([stringField]);
+    await waitForPromises();
+
+    expect(wrapper.text()).toContain('Issue');
+    expect(wrapper.text()).toContain('Task');
+  });
+
+  it('toggles details when detail button is clicked', async () => {
+    createComponent();
+    await waitForPromises();
+
+    expect(wrapper.text()).not.toContain('Last updated');
+    await findDetailsButton().trigger('click');
+
+    expect(wrapper.text()).toContain('Last updated');
+  });
+
+  it('renders TimeagoTooltip components with correct timestamps', async () => {
+    createComponent();
+    await waitForPromises();
+
+    const timeagoComponents = wrapper.findAllComponents(TimeagoTooltip);
+    expect(timeagoComponents.exists()).toBe(true);
+    expect(timeagoComponents.at(0).props('time')).toBe(selectField.updatedAt);
+  });
+});
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d318cf7f47ca3f66800e9cb5ad8475108904a766..ef1aa6688d6cbfcfb4473b4791ff3735cc55845b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -397,6 +397,11 @@ msgid_plural "%d more comments"
 msgstr[0] ""
 msgstr[1] ""
 
+msgid "%d option"
+msgid_plural "%d options"
+msgstr[0] ""
+msgstr[1] ""
+
 msgid "%d package"
 msgid_plural "%d packages"
 msgstr[0] ""
@@ -62365,6 +62370,9 @@ msgstr[1] ""
 msgid "WorkItem|A non-confidential %{workItemType} cannot be assigned to a confidential parent %{parentWorkItemType}."
 msgstr ""
 
+msgid "WorkItem|Active custom fields"
+msgstr ""
+
 msgid "WorkItem|Activity"
 msgstr ""
 
@@ -62464,6 +62472,9 @@ msgstr ""
 msgid "WorkItem|Comments only"
 msgstr ""
 
+msgid "WorkItem|Configure work items such as epics, issues, and tasks to represent how your team works."
+msgstr ""
+
 msgid "WorkItem|Convert to child item"
 msgstr ""
 
@@ -62476,6 +62487,15 @@ msgstr ""
 msgid "WorkItem|Create %{workItemType}"
 msgstr ""
 
+msgid "WorkItem|Created %{timeago}"
+msgstr ""
+
+msgid "WorkItem|Custom fields"
+msgstr ""
+
+msgid "WorkItem|Custom fields extend work items to track additional data. Fields will appear in alphanumeric order. All fields apply to all subgroups and projects."
+msgstr ""
+
 msgid "WorkItem|Dark red"
 msgstr ""
 
@@ -62497,6 +62517,9 @@ msgstr ""
 msgid "WorkItem|Due"
 msgstr ""
 
+msgid "WorkItem|Edit field"
+msgstr ""
+
 msgid "WorkItem|Epic"
 msgstr ""
 
@@ -62506,6 +62529,9 @@ msgstr ""
 msgid "WorkItem|Existing task"
 msgstr ""
 
+msgid "WorkItem|Field"
+msgstr ""
+
 msgid "WorkItem|Fixed"
 msgstr ""
 
@@ -62518,6 +62544,9 @@ msgstr ""
 msgid "WorkItem|History only"
 msgstr ""
 
+msgid "WorkItem|How do I use custom fields?"
+msgstr ""
+
 msgid "WorkItem|Incident"
 msgstr ""
 
@@ -62527,6 +62556,9 @@ msgstr ""
 msgid "WorkItem|Issue"
 msgstr ""
 
+msgid "WorkItem|Issues settings"
+msgstr ""
+
 msgid "WorkItem|Iteration"
 msgstr ""
 
@@ -62536,6 +62568,9 @@ msgstr ""
 msgid "WorkItem|Key result"
 msgstr ""
 
+msgid "WorkItem|Last updated %{timeago}"
+msgstr ""
+
 msgid "WorkItem|Lavender"
 msgstr ""
 
@@ -62626,6 +62661,9 @@ msgstr ""
 msgid "WorkItem|Open"
 msgstr ""
 
+msgid "WorkItem|Options:"
+msgstr ""
+
 msgid "WorkItem|Parent"
 msgstr ""
 
@@ -62815,15 +62853,27 @@ msgstr ""
 msgid "WorkItem|This work item is not available. It either doesn't exist or you don't have permission to view it."
 msgstr ""
 
+msgid "WorkItem|Toggle details"
+msgstr ""
+
 msgid "WorkItem|Turn off confidentiality"
 msgstr ""
 
 msgid "WorkItem|Turn on confidentiality"
 msgstr ""
 
+msgid "WorkItem|Type"
+msgstr ""
+
 msgid "WorkItem|Undo"
 msgstr ""
 
+msgid "WorkItem|Usage"
+msgstr ""
+
+msgid "WorkItem|Usage:"
+msgstr ""
+
 msgid "WorkItem|View current version"
 msgstr ""