diff --git a/app/controllers/concerns/groups/params.rb b/app/controllers/concerns/groups/params.rb
index 7649dc971171e4fcc91650b86be379e6109a77cb..eafc24524bc3f110c9728bb0860c50c6bb6fc6ef 100644
--- a/app/controllers/concerns/groups/params.rb
+++ b/app/controllers/concerns/groups/params.rb
@@ -19,6 +19,7 @@ def group_params_attributes
         :emails_disabled,
         :emails_enabled,
         :show_diff_preview_in_email,
+        :token_expiry_notify_inherited,
         :mentions_disabled,
         :lfs_enabled,
         :name,
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 55de4fac24b27ae574d58fe8c4d0d69baba3b263..4bbb5100b156b703a83ac27780591dfc9734e2e5 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -173,6 +173,7 @@ class Namespace < ApplicationRecord
     to: :namespace_settings
   delegate :emails_enabled, :emails_enabled=,
     to: :namespace_settings, allow_nil: true
+  delegate :token_expiry_notify_inherited, :token_expiry_notify_inherited=, to: :namespace_settings
   delegate :allow_runner_registration_token,
     :allow_runner_registration_token=,
     to: :namespace_settings
@@ -618,6 +619,14 @@ def closest_setting(name)
       .try(name)
   end
 
+  def can_modify_token_expiry_notify_inherited?
+    ancestors.all?(&:token_expiry_notify_inherited)
+  end
+
+  def token_expiry_notify_inherited?
+    self_and_ancestors.all?(&:token_expiry_notify_inherited)
+  end
+
   def actual_plan
     Plan.default
   end
diff --git a/app/views/groups/settings/_email_settings.html.haml b/app/views/groups/settings/_email_settings.html.haml
index bed7709bba785abe031018dd84c1792f1562968e..aa4e7f4e3d0e481a5455f06bc3e297a0f06c14b2 100644
--- a/app/views/groups/settings/_email_settings.html.haml
+++ b/app/views/groups/settings/_email_settings.html.haml
@@ -11,3 +11,12 @@
         checkbox_options: { checked: @group.show_diff_preview_in_email? & @group.emails_enabled?,
                           disabled: !@group.emails_enabled? || !can_set_group_diff_preview_in_email?(@group) },
         help_text: s_('GroupSettings|Emails are not encrypted. Concerned administrators may want to disable diff previews.')
+
+- if Feature.enabled?(:pat_expiry_inherited_members_notification, @group.root_ancestor)
+  - controlled_by_parent_group = !@group.can_modify_token_expiry_notify_inherited?
+  .form-group
+    = f.label :token_expiry_notify_inherited, _('Expiry notification emails about group and project access tokens within this group should be sent to:')
+    = f.gitlab_ui_radio_component :token_expiry_notify_inherited, true, _('All direct and inherited members of the group or project'), radio_options: { disabled: controlled_by_parent_group }
+    = f.gitlab_ui_radio_component :token_expiry_notify_inherited, false, _('Only direct members of the group or project'), radio_options: { disabled: controlled_by_parent_group }
+    - if controlled_by_parent_group
+      .form-text= _('A parent group has selected "Only direct members." It cannot be overridden by this group.')
diff --git a/db/migrate/20241004213405_add_token_expiry_notify_inherited_members_group_setting.rb b/db/migrate/20241004213405_add_token_expiry_notify_inherited_members_group_setting.rb
new file mode 100644
index 0000000000000000000000000000000000000000..dfef9c36edfac77fa50c6a80338e597925e83291
--- /dev/null
+++ b/db/migrate/20241004213405_add_token_expiry_notify_inherited_members_group_setting.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddTokenExpiryNotifyInheritedMembersGroupSetting < Gitlab::Database::Migration[2.2]
+  milestone '17.6'
+
+  def change
+    add_column :namespace_settings, :token_expiry_notify_inherited, :boolean, default: true, null: false
+  end
+end
diff --git a/db/schema_migrations/20241004213405 b/db/schema_migrations/20241004213405
new file mode 100644
index 0000000000000000000000000000000000000000..ddad2f8a7ccab906b091bf1120e1eac48c23a56b
--- /dev/null
+++ b/db/schema_migrations/20241004213405
@@ -0,0 +1 @@
+fdc6b0a8a43515c7a0dd89e650e5ebcd95d78f1911dee2294a0ad891c0e9b6ce
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index b750f040657432737fc77b8a452f28737964a58a..d55cebdf34bcb8027bb55cf573d48a649841b27b 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -14612,6 +14612,7 @@ CREATE TABLE namespace_settings (
     spp_repository_pipeline_access boolean,
     lock_spp_repository_pipeline_access boolean DEFAULT false NOT NULL,
     archived boolean DEFAULT false NOT NULL,
+    token_expiry_notify_inherited boolean DEFAULT true NOT NULL,
     CONSTRAINT check_0ba93c78c7 CHECK ((char_length(default_branch_name) <= 255)),
     CONSTRAINT namespace_settings_unique_project_download_limit_alertlist_size CHECK ((cardinality(unique_project_download_limit_alertlist) <= 100)),
     CONSTRAINT namespace_settings_unique_project_download_limit_allowlist_size CHECK ((cardinality(unique_project_download_limit_allowlist) <= 100))
diff --git a/ee/spec/lib/audit/namespace_setting_changes_auditor_spec.rb b/ee/spec/lib/audit/namespace_setting_changes_auditor_spec.rb
index 5c7a861973808af9137a9fcc5618f8b155e2e938..adc6fa5f9434e1da3f83aaae9db311b01d9a789a 100644
--- a/ee/spec/lib/audit/namespace_setting_changes_auditor_spec.rb
+++ b/ee/spec/lib/audit/namespace_setting_changes_auditor_spec.rb
@@ -121,7 +121,7 @@
         default_branch_protection_defaults allow_merge_without_pipeline auto_ban_user_on_excessive_projects_download
         lock_math_rendering_limits_enabled enable_auto_assign_gitlab_duo_pro_seats early_access_program_participant
         lock_duo_features_enabled allow_merge_without_pipeline only_allow_merge_if_pipeline_succeeds
-        lock_spp_repository_pipeline_access spp_repository_pipeline_access archived]
+        lock_spp_repository_pipeline_access spp_repository_pipeline_access archived token_expiry_notify_inherited]
 
       columns_to_audit = Audit::NamespaceSettingChangesAuditor::EVENT_NAME_PER_COLUMN.keys.map(&:to_s)
 
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 0ec0594ffb1a68384557b895eeba3c640ed5b016..741050dd9190c7d8609554ecbf518e2a78ddaa13 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2089,6 +2089,9 @@ msgstr ""
 msgid "A non-confidential work item cannot have a confidential parent."
 msgstr ""
 
+msgid "A parent group has selected \"Only direct members.\" It cannot be overridden by this group."
+msgstr ""
+
 msgid "A parent must be provided when bulk updating issuables"
 msgstr ""
 
@@ -22688,6 +22691,9 @@ msgstr ""
 msgid "Expiry notification emails about group and project access tokens should be sent to:"
 msgstr ""
 
+msgid "Expiry notification emails about group and project access tokens within this group should be sent to:"
+msgstr ""
+
 msgid "Explain current vulnerability."
 msgstr ""
 
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 1e93f5a415599571b4472e3b238d7d5e3b8ffcd9..4f0ef85fc166cc84f42fe71e4f98bbaae637b1eb 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -654,6 +654,8 @@
     it { is_expected.to delegate_method(:math_rendering_limits_enabled).to(:namespace_settings) }
     it { is_expected.to delegate_method(:math_rendering_limits_enabled?).to(:namespace_settings) }
     it { is_expected.to delegate_method(:lock_math_rendering_limits_enabled?).to(:namespace_settings) }
+    it { is_expected.to delegate_method(:token_expiry_notify_inherited).to(:namespace_settings) }
+    it { is_expected.to delegate_method(:token_expiry_notify_inherited=).to(:namespace_settings).with_arguments(:args) }
     it { is_expected.to delegate_method(:add_creator).to(:namespace_details) }
     it { is_expected.to delegate_method(:pending_delete).to(:namespace_details) }
     it { is_expected.to delegate_method(:pending_delete=).to(:namespace_details).with_arguments(:args) }
@@ -2221,6 +2223,74 @@
     end
   end
 
+  context 'with token_expiry_notify_inherited settings' do
+    let_it_be_with_reload(:grandparent_namespace) { create(:group) }
+    let_it_be_with_reload(:parent_namespace) { create(:group, parent: grandparent_namespace) }
+    let_it_be_with_reload(:child_namespace) { create(:group, parent: parent_namespace) }
+
+    describe '.token_expiry_notify_inherited?' do
+      subject { child_namespace.token_expiry_notify_inherited? }
+
+      # setting defaults to true for all namespace settings
+      it { is_expected.to eq(true) }
+
+      context 'when parent namespace has setting disabled' do
+        before do
+          parent_namespace.namespace_settings.update!(token_expiry_notify_inherited: false)
+        end
+
+        it { is_expected.to eq(false) }
+      end
+
+      context 'when grandparent namespace has setting disabled' do
+        before do
+          grandparent_namespace.namespace_settings.update!(token_expiry_notify_inherited: false)
+        end
+
+        it { is_expected.to eq(false) }
+      end
+
+      context 'when current token_expiry_notify_inherited is set to false' do
+        before do
+          child_namespace.namespace_settings.update!(token_expiry_notify_inherited: false)
+        end
+
+        it { is_expected.to eq(false) }
+      end
+    end
+
+    describe '.can_modify_token_expiry_notify_inherited?' do
+      subject { child_namespace.can_modify_token_expiry_notify_inherited? }
+
+      # setting defaults to true for all namespace settings
+      it { is_expected.to eq(true) }
+
+      context 'when parent namespace has setting disabled' do
+        before do
+          parent_namespace.namespace_settings.update!(token_expiry_notify_inherited: false)
+        end
+
+        it { is_expected.to eq(false) }
+      end
+
+      context 'when grandparent namespace has setting disabled' do
+        before do
+          grandparent_namespace.namespace_settings.update!(token_expiry_notify_inherited: false)
+        end
+
+        it { is_expected.to eq(false) }
+      end
+
+      context 'when current token_expiry_notify_inherited is set to false' do
+        before do
+          child_namespace.namespace_settings.update!(token_expiry_notify_inherited: false)
+        end
+
+        it { is_expected.to eq(true) }
+      end
+    end
+  end
+
   describe '#any_project_with_pages_deployed?' do
     it 'returns true if any project nested under the group has pages deployed' do
       parent_1 = create(:group) # Three projects, one with pages
diff --git a/spec/views/groups/edit.html.haml_spec.rb b/spec/views/groups/edit.html.haml_spec.rb
index f33a4190bf8e40e1bcb17192f2da80c867f104dd..029531c58449d5a216660578a8b74930b244603d 100644
--- a/spec/views/groups/edit.html.haml_spec.rb
+++ b/spec/views/groups/edit.html.haml_spec.rb
@@ -161,4 +161,50 @@
       end
     end
   end
+
+  describe 'Email notifications section' do
+    let_it_be(:user) { create(:user) }
+    let_it_be_with_reload(:parent_group) { create(:group, owners: user) }
+    let_it_be_with_reload(:group) { create(:group, parent: parent_group, owners: user) }
+
+    before do
+      assign(:group, group)
+      allow(view).to receive(:current_user).and_return(user)
+    end
+
+    it 'renders fields for token_expiry_notify_inherited' do
+      render
+
+      expect(rendered).to have_content(_('Expiry notification emails about group and project access tokens within this group should be sent to:'))
+      expect(rendered).to have_selector('#group_token_expiry_notify_inherited_true:not([disabled])')
+      expect(rendered).to have_selector('#group_token_expiry_notify_inherited_false:not([disabled])')
+      expect(rendered).not_to have_content(_('A parent group has selected "Only direct members." It cannot be overridden by this group.'))
+    end
+
+    context 'when pat_expiry_inherited_members_notification FF is disabled' do
+      before do
+        stub_feature_flags(pat_expiry_inherited_members_notification: false)
+      end
+
+      it 'does not render form' do
+        render
+
+        expect(rendered).not_to have_content(_('Expiry notification emails about group and project access tokens within this group should be sent to:'))
+      end
+    end
+
+    context 'when parent group has token_expiry_notify_inherited set to false' do
+      before do
+        parent_group.namespace_settings.update!(token_expiry_notify_inherited: false)
+      end
+
+      it 'renders disabled fields' do
+        render
+
+        expect(rendered).to have_selector('#group_token_expiry_notify_inherited_false[disabled]')
+        expect(rendered).to have_selector('#group_token_expiry_notify_inherited_true[disabled]')
+        expect(rendered).to have_content(_('A parent group has selected "Only direct members." It cannot be overridden by this group.'))
+      end
+    end
+  end
 end