diff --git src/bp-core/admin/bp-core-admin-functions.php src/bp-core/admin/bp-core-admin-functions.php
index b81786427..7749dd449 100644
--- src/bp-core/admin/bp-core-admin-functions.php
+++ src/bp-core/admin/bp-core-admin-functions.php
@@ -84,6 +84,11 @@ function bp_core_modify_admin_menu_highlight() {
 	if ( in_array( $plugin_page, array( 'bp-tools', 'available-tools' ) ) ) {
 		$submenu_file = $plugin_page;
 	}
+
+	// Keep the BuddyPress tools menu highlighted.
+	if ( 'bp-optouts' === $plugin_page ) {
+		$submenu_file = 'bp-tools';
+	}
 }
 
 /**
@@ -389,10 +394,13 @@ function bp_do_activation_redirect() {
  * Output the tabs in the admin area.
  *
  * @since 1.5.0
+ * @since 8.0.0 Adds the `$context` parameter.
  *
  * @param string $active_tab Name of the tab that is active. Optional.
+ * @param string $context    The context of use for the tabs. Defaults to 'settings'.
+ *                           Possible values are 'settings' & 'tools'.
  */
-function bp_core_admin_tabs( $active_tab = '' ) {
+function bp_core_admin_tabs( $active_tab = '', $context = 'settings' ) {
 	$tabs_html    = '';
 	$idle_class   = 'nav-tab';
 	$active_class = 'nav-tab nav-tab-active';
@@ -404,7 +412,7 @@ function bp_core_admin_tabs( $active_tab = '' ) {
 	 *
 	 * @param array $value Array of tabs to output to the admin area.
 	 */
-	$tabs = apply_filters( 'bp_core_admin_tabs', bp_core_get_admin_tabs( $active_tab ) );
+	$tabs = apply_filters( 'bp_core_admin_tabs', bp_core_get_admin_tabs( $active_tab, $context ) );
 
 	// Loop through tabs and build navigation.
 	foreach ( array_values( $tabs ) as $tab_data ) {
@@ -419,46 +427,74 @@ function bp_core_admin_tabs( $active_tab = '' ) {
 	 * Fires after the output of tabs for the admin area.
 	 *
 	 * @since 1.5.0
+	 * @since 8.0.0 Adds the `$context` parameter.
+	 *
+	 * @param string $context The context of use for the tabs.
 	 */
-	do_action( 'bp_admin_tabs' );
+	do_action( 'bp_admin_tabs', $context );
 }
 
 /**
  * Get the data for the tabs in the admin area.
  *
  * @since 2.2.0
+ * @since 8.0.0 Adds the `$context` parameter.
  *
  * @param string $active_tab Name of the tab that is active. Optional.
+ * @param string $context    The context of use for the tabs. Defaults to 'settings'.
+ *                           Possible values are 'settings' & 'tools'.
  * @return string
  */
-function bp_core_get_admin_tabs( $active_tab = '' ) {
-	$tabs = array(
-		'0' => array(
-			'href' => bp_get_admin_url( add_query_arg( array( 'page' => 'bp-components' ), 'admin.php' ) ),
-			'name' => __( 'Components', 'buddypress' ),
-		),
-		'2' => array(
-			'href' => bp_get_admin_url( add_query_arg( array( 'page' => 'bp-settings' ), 'admin.php' ) ),
-			'name' => __( 'Options', 'buddypress' ),
-		),
-		'1' => array(
-			'href' => bp_get_admin_url( add_query_arg( array( 'page' => 'bp-page-settings' ), 'admin.php' ) ),
-			'name' => __( 'Pages', 'buddypress' ),
-		),
-		'3' => array(
-			'href' => bp_get_admin_url( add_query_arg( array( 'page' => 'bp-credits' ), 'admin.php' ) ),
-			'name' => __( 'Credits', 'buddypress' ),
-		),
-	);
+function bp_core_get_admin_tabs( $active_tab = '', $context = 'settings' ) {
+	$tabs = array();
+
+	if ( 'settings' === $context ) {
+		$tabs = array(
+			'0' => array(
+				'href' => bp_get_admin_url( add_query_arg( array( 'page' => 'bp-components' ), 'admin.php' ) ),
+				'name' => __( 'Components', 'buddypress' ),
+			),
+			'2' => array(
+				'href' => bp_get_admin_url( add_query_arg( array( 'page' => 'bp-settings' ), 'admin.php' ) ),
+				'name' => __( 'Options', 'buddypress' ),
+			),
+			'1' => array(
+				'href' => bp_get_admin_url( add_query_arg( array( 'page' => 'bp-page-settings' ), 'admin.php' ) ),
+				'name' => __( 'Pages', 'buddypress' ),
+			),
+			'3' => array(
+				'href' => bp_get_admin_url( add_query_arg( array( 'page' => 'bp-credits' ), 'admin.php' ) ),
+				'name' => __( 'Credits', 'buddypress' ),
+			),
+		);
+	} elseif ( 'tools' === $context ) {
+		$tools_page = 'tools.php';
+		if ( bp_core_do_network_admin() ) {
+			$tools_page = 'admin.php';
+		}
+
+		$tabs = array(
+			'0' => array(
+				'href' => bp_get_admin_url( add_query_arg( array( 'page' => 'bp-tools' ), $tools_page ) ),
+				'name' => __( 'Repair', 'buddypress' ),
+			),
+			'1' => array(
+				'href' => bp_get_admin_url( add_query_arg( array( 'page' => 'bp-optouts' ), $tools_page ) ),
+				'name' => __( 'Manage Opt-outs', 'buddypress' ),
+			),
+		);
+	}
 
 	/**
 	 * Filters the tab data used in our wp-admin screens.
 	 *
 	 * @since 2.2.0
+	 * @since 8.0.0 Adds the `$context` parameter.
 	 *
-	 * @param array $tabs Tab data.
+	 * @param array  $tabs    Tab data.
+	 * @param string $context The context of use for the tabs.
 	 */
-	return apply_filters( 'bp_core_get_admin_tabs', $tabs );
+	return apply_filters( 'bp_core_get_admin_tabs', $tabs, $context );
 }
 
 /** Help **********************************************************************/
diff --git src/bp-core/admin/bp-core-admin-optouts.php src/bp-core/admin/bp-core-admin-optouts.php
new file mode 100644
index 000000000..68fcf3c3c
--- /dev/null
+++ src/bp-core/admin/bp-core-admin-optouts.php
@@ -0,0 +1,453 @@
+<?php
+/**
+ * BuddyPress Opt-outs management.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ * @since 8.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Set up the Opt-outs admin page.
+ *
+ * Loaded before the page is rendered, this function does all initial
+ * setup, including: processing form requests, registering contextual
+ * help, and setting up screen options.
+ *
+ * @since 8.0.0
+ *
+ * @global $bp_optouts_list_table
+ */
+function bp_core_optouts_admin_load() {
+	global $bp_optouts_list_table;
+
+	// Build redirection URL.
+	$redirect_to = remove_query_arg( array( 'action', 'error', 'updated', 'activated', 'notactivated', 'deleted', 'notdeleted', 'resent', 'notresent', 'do_delete', 'do_resend', 'do_activate', '_wpnonce', 'signup_ids' ), $_SERVER['REQUEST_URI'] );
+	$doaction    = bp_admin_list_table_current_bulk_action();
+
+	/**
+	 * Fires at the start of the nonmember opt-outs admin load.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @param string $doaction Current bulk action being processed.
+	 * @param array  $_REQUEST Current $_REQUEST global.
+	 */
+	do_action( 'bp_optouts_admin_load', $doaction, $_REQUEST );
+
+	/**
+	 * Filters the allowed actions for use in the nonmember optouts admin page.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @param array $value Array of allowed actions to use.
+	 */
+	$allowed_actions = apply_filters( 'bp_optouts_admin_allowed_actions', array( 'do_delete',  'do_resend' ) );
+
+	if ( ! in_array( $doaction, $allowed_actions ) || ( -1 == $doaction ) ) {
+
+		require_once( ABSPATH . 'wp-admin/includes/class-wp-users-list-table.php' );
+		$bp_optouts_list_table = new BP_Optouts_List_Table();
+
+		// The per_page screen option.
+		add_screen_option( 'per_page', array( 'label' => _x( 'Nonmember opt-outs', 'Nonmember opt-outs per page (screen options)', 'buddypress' ) ) );
+
+		// Current screen.
+		$current_screen = get_current_screen();
+
+		$current_screen->add_help_tab(
+			array(
+				'id'      => 'bp-optouts-overview',
+				'title'   => __( 'Overview', 'buddypress' ),
+				'content' =>
+					'<p>' . __( 'This is the administration screen for nonmember opt-outs on your site.', 'buddypress' ) . '</p>' .
+					'<p>' . __( 'From the screen options, you can customize the displayed columns and the pagination of this screen.', 'buddypress' ) . '</p>' .
+					'<p>' . __( 'You can reorder the list of opt-outs by clicking on the Email Sender, Email Type or Date Modified column headers.', 'buddypress' ) . '</p>' .
+					'<p>' . __( 'Using the search form, you can search for an opt-out to a specific email address.', 'buddypress' ) . '</p>',
+			)
+		);
+
+		$current_screen->add_help_tab(
+			array(
+				'id'      => 'bp-optouts-actions',
+				'title'   => __( 'Actions', 'buddypress' ),
+				'content' =>
+					'<p>' . __( 'Hovering over a row in the opt-outs list will display action links that allow you to manage the opt-out. You can perform the following actions:', 'buddypress' ) . '</p>' .
+					'<ul><li>' . __( '"Delete" allows you to delete the record of an opt-out. You will be asked to confirm this deletion.', 'buddypress' ) . '</li></ul>' .
+					'<p>' . __( 'Bulk actions allow you to perform these actions for the selected rows.', 'buddypress' ) . '</p>',
+			)
+		);
+
+		// Help panel - sidebar links.
+		$current_screen->set_help_sidebar(
+			'<p><strong>' . __( 'For more information:', 'buddypress' ) . '</strong></p>' .
+			'<p>' . __( '<a href="https://buddypress.org/support/">Support Forums</a>', 'buddypress' ) . '</p>'
+		);
+
+		// Add accessible hidden headings and text for the Pending Users screen.
+		$current_screen->set_screen_reader_content(
+			array(
+				/* translators: accessibility text */
+				'heading_views'      => __( 'Filter opt-outs list', 'buddypress' ),
+				/* translators: accessibility text */
+				'heading_pagination' => __( 'Opt-out list navigation', 'buddypress' ),
+				/* translators: accessibility text */
+				'heading_list'       => __( 'Opt-outs list', 'buddypress' ),
+			)
+		);
+
+	} else {
+		if ( empty( $_REQUEST['optout_ids' ] ) ) {
+			return;
+		}
+		$optout_ids = wp_parse_id_list( $_REQUEST['optout_ids' ] );
+
+		// Handle optout deletion.
+		if ( 'do_delete' == $doaction ) {
+
+			// Nonce check.
+			check_admin_referer( 'optouts_delete' );
+
+			$success = 0;
+			foreach ( $optout_ids as $optout_id ) {
+				if ( bp_delete_optout_by_id( $optout_id ) ) {
+					$success++;
+				}
+			}
+
+			$query_arg = array( 'updated' => 'deleted' );
+
+			if ( ! empty( $success ) ) {
+				$query_arg['deleted'] = $success;
+			}
+
+			$notdeleted = count( $optout_ids ) - $success;
+			if ( $notdeleted > 0 ) {
+				$query_arg['notdeleted'] = $notdeleted;
+			}
+
+			$redirect_to = add_query_arg( $query_arg, $redirect_to );
+
+			bp_core_redirect( $redirect_to );
+
+		// Plugins can update other stuff from here.
+		} else {
+
+			/**
+			 * Fires at end of opt-outs admin load
+			 * if doaction does not match any actions.
+			 *
+			 * @since 2.0.0
+			 *
+			 * @param string $doaction Current bulk action being processed.
+			 * @param array  $_REQUEST Current $_REQUEST global.
+			 * @param string $redirect Determined redirect url to send user to.
+			 */
+			do_action( 'bp_core_admin_update_optouts', $doaction, $_REQUEST, $redirect_to );
+
+			bp_core_redirect( $redirect_to );
+		}
+	}
+}
+add_action( "load-tools_page_bp-optouts", 'bp_core_optouts_admin_load' );
+
+/**
+ * Get admin notice when viewing the optouts management page.
+ *
+ * @since 8.0.0
+ *
+ * @return array
+ */
+function bp_core_get_optouts_notice() {
+
+	// Setup empty notice for return value.
+	$notice = array();
+
+	// Updates.
+	if ( ! empty( $_REQUEST['updated'] ) && 'deleted' === $_REQUEST['updated'] ) {
+		$notice = array(
+			'class'   => 'updated',
+			'message' => ''
+		);
+
+		if ( ! empty( $_REQUEST['deleted'] ) ) {
+			$notice['message'] .= sprintf(
+				/* translators: %s: number of deleted optouts */
+				_nx( '%s opt-out successfully deleted!', '%s opt-outs successfully deleted!',
+				 absint( $_REQUEST['deleted'] ),
+				 'nonmembers optout deleted',
+				 'buddypress'
+				),
+				number_format_i18n( absint( $_REQUEST['deleted'] ) )
+			);
+		}
+
+		if ( ! empty( $_REQUEST['notdeleted'] ) ) {
+			$notice['message'] .= sprintf(
+				/* translators: %s: number of optouts that failed to be deleted */
+				_nx( '%s opt-out was not deleted.', '%s opt-outs were not deleted.',
+				 absint( $_REQUEST['notdeleted'] ),
+				 'nonmembers optout not deleted',
+				 'buddypress'
+				),
+				number_format_i18n( absint( $_REQUEST['notdeleted'] ) )
+			);
+
+			if ( empty( $_REQUEST['deleted'] ) ) {
+				$notice['class'] = 'error';
+			}
+		}
+	}
+
+	// Errors.
+	if ( ! empty( $_REQUEST['error'] ) && 'do_delete' === $_REQUEST['error'] ) {
+		$notice = array(
+			'class'   => 'error',
+			'message' => esc_html__( 'There was a problem deleting opt-outs. Please try again.', 'buddypress' ),
+		);
+	}
+
+	return $notice;
+}
+
+/**
+ * Opt-outs admin page router.
+ *
+ * Depending on the context, display
+ * - the list of optouts,
+ * - or the delete confirmation screen,
+ *
+ * Also prepare the admin notices.
+ *
+ * @since 8.0.0
+ */
+function bp_core_optouts_admin() {
+	$doaction = bp_admin_list_table_current_bulk_action();
+
+	// Prepare notices for admin.
+	$notice = bp_core_get_optouts_notice();
+
+	// Display notices.
+	if ( ! empty( $notice ) ) :
+		if ( 'updated' === $notice['class'] ) : ?>
+
+			<div id="message" class="<?php echo esc_attr( $notice['class'] ); ?> notice is-dismissible">
+
+		<?php else: ?>
+
+			<div class="<?php echo esc_attr( $notice['class'] ); ?> notice is-dismissible">
+
+		<?php endif; ?>
+
+			<p><?php echo esc_html( $notice['message'] ); ?></p>
+		</div>
+
+	<?php endif;
+
+	// Show the proper screen.
+	switch ( $doaction ) {
+		case 'delete' :
+			bp_core_optouts_admin_manage( $doaction );
+			break;
+
+		default:
+			bp_core_optouts_admin_index();
+			break;
+	}
+}
+
+/**
+ * This is the list of optouts.
+ *
+ * @since 8.0.0
+ *
+ * @global $plugin_page
+ * @global $bp_optouts_list_table
+ */
+function bp_core_optouts_admin_index() {
+	global $plugin_page, $bp_optouts_list_table;
+
+	$usersearch = ! empty( $_REQUEST['s'] ) ? stripslashes( $_REQUEST['s'] ) : '';
+
+	// Prepare the group items for display.
+	$bp_optouts_list_table->prepare_items();
+
+	if ( is_network_admin() ) {
+		$form_url = network_admin_url( 'admin.php' );
+	} else {
+		$form_url = bp_get_admin_url( 'tools.php' );
+	}
+
+	$form_url = add_query_arg(
+		array(
+			'page' => 'bp-optouts',
+		),
+		$form_url
+	);
+
+	$search_form_url = remove_query_arg(
+		array(
+			'action',
+			'deleted',
+			'notdeleted',
+			'error',
+			'updated',
+			'delete',
+			'activate',
+			'activated',
+			'notactivated',
+			'resend',
+			'resent',
+			'notresent',
+			'do_delete',
+			'do_activate',
+			'do_resend',
+			'action2',
+			'_wpnonce',
+			'optout_ids'
+		),
+		$_SERVER['REQUEST_URI']
+	);
+
+	?>
+
+	<div class="wrap">
+		<h1 class="wp-heading-inline"><?php esc_html_e( 'BuddyPress tools', 'buddypress' ); ?></h1>
+		<hr class="wp-header-end">
+
+		<h2 class="nav-tab-wrapper"><?php bp_core_admin_tabs( __( 'Manage Opt-outs', 'buddypress' ), 'tools' ); ?></h2>
+
+		<?php
+		if ( $usersearch ) {
+			$num_results = (int) $bp_optouts_list_table->total_items;
+			printf( '<p><span class="subtitle">' . esc_html( _n( 'Opt-out with an email address matching &#8220;%s&#8221;', 'Opt-outs with an email address matching &#8220;%s&#8221;', $num_results, 'buddypress' ) ) . '</span></p>', esc_html( $usersearch ) );
+		}
+		?>
+		<p><?php esc_html_e( 'This table shows opt-out requests from people who are not members of this site, but have been contacted via communication from this site, and wish to receive no further communications.', 'buddypress' ); ?></p>
+
+		<?php // Display each opt-out on its own row. ?>
+		<?php $bp_optouts_list_table->views(); ?>
+
+		<form id="bp-optouts-search-form" action="<?php echo esc_url( $search_form_url ) ;?>">
+			<input type="hidden" name="page" value="<?php echo esc_attr( $plugin_page ); ?>" />
+			<?php $bp_optouts_list_table->search_box( esc_html__( 'Search for a specific email address', 'buddypress' ), 'bp-optouts' ); ?>
+		</form>
+
+		<form id="bp-optouts-form" action="<?php echo esc_url( $form_url );?>" method="post">
+			<?php $bp_optouts_list_table->display(); ?>
+		</form>
+	</div>
+<?php
+}
+
+/**
+ * This is the confirmation screen for actions.
+ *
+ * @since 8.0.0
+ *
+ * @param string $action Delete or resend optout.
+ *
+ * @return null|false
+ */
+function bp_core_optouts_admin_manage( $action = '' ) {
+	$capability = bp_core_do_network_admin() ? 'manage_network_options' : 'manage_options';
+	if ( ! current_user_can( $capability ) || empty( $action ) ) {
+		die( '-1' );
+	}
+
+	// Get the IDs from the URL.
+	$ids = false;
+	if ( ! empty( $_POST['optout_ids'] ) ) {
+		$ids = wp_parse_id_list( $_POST['optout_ids'] );
+	} elseif ( ! empty( $_GET['optout_id'] ) ) {
+		$ids = absint( $_GET['optout_id'] );
+	}
+
+	if ( empty( $ids ) ) {
+		return false;
+	}
+
+	// Query for matching optouts, and filter out bad IDs.
+	$args = array(
+		'id'     => $ids,
+	);
+	$optouts    = bp_get_optouts( $args );
+	$optout_ids = wp_list_pluck( $optouts, 'id' );
+
+	// Check optout IDs and set up strings.
+	switch ( $action ) {
+		case 'delete' :
+			if ( 1 == count( $optouts ) ) {
+				$helper_text = __( 'You are about to delete the following opt-out request:', 'buddypress' );
+			} else {
+				$helper_text = __( 'You are about to delete the following opt-out requests:', 'buddypress' );
+			}
+			break;
+	}
+
+	// These arguments are added to all URLs.
+	$url_args = array( 'page' => 'bp-optouts' );
+
+	// These arguments are only added when performing an action.
+	$action_args = array(
+		'action'     => 'do_' . $action,
+		'optout_ids' => implode( ',', $optout_ids )
+	);
+
+	if ( is_network_admin() ) {
+		$base_url = network_admin_url( 'admin.php' );
+	} else {
+		$base_url = bp_get_admin_url( 'tools.php' );
+	}
+
+	$cancel_url = add_query_arg( $url_args, $base_url );
+	$action_url = wp_nonce_url(
+		add_query_arg(
+			array_merge( $url_args, $action_args ),
+			$base_url
+		),
+		'optouts_' . $action
+	);
+
+	?>
+
+	<div class="wrap">
+		<h1 class="wp-heading-inline"><?php esc_html_e( 'BuddyPress tools', 'buddypress' ); ?></h1>
+		<hr class="wp-header-end">
+
+		<h2 class="nav-tab-wrapper"><?php bp_core_admin_tabs( __( 'Manage Opt-outs', 'buddypress' ), 'tools' ); ?></h2>
+
+		<p><?php echo esc_html( $helper_text ); ?></p>
+
+		<ol class="bp-optouts-list">
+		<?php foreach ( $optouts as $optout ) :	?>
+
+			<li>
+				<strong><?php echo esc_html( $optout->email_address ) ?></strong>
+				<p class="description">
+					<?php
+					$last_modified = mysql2date( 'Y/m/d g:i:s a', $optout->date_modified );
+					/* translators: %s: modification date */
+					printf( esc_html__( 'Date modified: %s', 'buddypress'), $last_modified );
+					?>
+				</p>
+			</li>
+
+		<?php endforeach; ?>
+		</ol>
+
+		<?php if ( 'delete' === $action ) : ?>
+
+			<p><strong><?php esc_html_e( 'This action cannot be undone.', 'buddypress' ) ?></strong></p>
+
+		<?php endif ; ?>
+
+		<a class="button-primary" href="<?php echo esc_url( $action_url ); ?>"><?php esc_html_e( 'Confirm', 'buddypress' ); ?></a>
+		<a class="button" href="<?php echo esc_url( $cancel_url ); ?>"><?php esc_html_e( 'Cancel', 'buddypress' ) ?></a>
+	</div>
+
+	<?php
+}
diff --git src/bp-core/admin/bp-core-admin-schema.php src/bp-core/admin/bp-core-admin-schema.php
index 7a10e39eb..2dde95a4a 100644
--- src/bp-core/admin/bp-core-admin-schema.php
+++ src/bp-core/admin/bp-core-admin-schema.php
@@ -40,6 +40,9 @@ function bp_core_install( $active_components = false ) {
 	// Install the invitations table.
 	bp_core_install_invitations();
 
+	// Install the nonmember opt-outs table.
+	bp_core_install_nonmember_opt_outs();
+
 	// Notifications.
 	if ( !empty( $active_components['notifications'] ) ) {
 		bp_core_install_notifications();
@@ -590,3 +593,38 @@ function bp_core_install_invitations() {
 	 */
 	do_action( 'bp_core_install_invitations' );
 }
+
+/**
+ * Install database tables to store opt-out requests from nonmembers.
+ *
+ * @since 8.0.0
+ *
+ * @uses bp_core_set_charset()
+ * @uses bp_core_get_table_prefix()
+ * @uses dbDelta()
+ */
+function bp_core_install_nonmember_opt_outs() {
+	$sql             = array();
+	$charset_collate = $GLOBALS['wpdb']->get_charset_collate();
+	$bp_prefix       = bp_core_get_table_prefix();
+	$optouts_class   = new BP_Optout();
+	$table_name      = $optouts_class->get_table_name();
+	$sql = "CREATE TABLE {$table_name} (
+		id bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+		email_address_hash varchar(255) NOT NULL,
+		user_id bigint(20) NOT NULL,
+		email_type varchar(255) NOT NULL,
+		date_modified datetime NOT NULL,
+		KEY user_id (user_id),
+		KEY email_type (email_type),
+		KEY date_modified (date_modified)
+		) {$charset_collate};";
+	dbDelta( $sql );
+
+	/**
+	 * Fires after BuddyPress adds the nonmember opt-outs table.
+	 *
+	 * @since 8.0.0
+	 */
+	do_action( 'bp_core_install_nonmember_opt_outs' );
+}
diff --git src/bp-core/admin/bp-core-admin-tools.php src/bp-core/admin/bp-core-admin-tools.php
index 0d49c0d6f..8141eccbe 100644
--- src/bp-core/admin/bp-core-admin-tools.php
+++ src/bp-core/admin/bp-core-admin-tools.php
@@ -22,6 +22,8 @@ function bp_core_admin_tools() {
 		<h1 class="wp-heading-inline"><?php esc_html_e( 'BuddyPress Tools', 'buddypress' ) ?></h1>
 		<hr class="wp-header-end">
 
+		<h2 class="nav-tab-wrapper"><?php bp_core_admin_tabs( __( 'Repair', 'buddypress' ), 'tools' ); ?></h2>
+
 		<p><?php esc_html_e( 'BuddyPress keeps track of various relationships between members, groups, and activity items.', 'buddypress' ); ?></p>
 		<p><?php esc_html_e( 'Occasionally these relationships become out of sync, most often after an import, update, or migration.', 'buddypress' ); ?></p>
 		<p><?php esc_html_e( 'Use the tools below to manually recalculate these relationships.', 'buddypress' ); ?>
@@ -541,18 +543,34 @@ function bp_core_admin_available_tools_intro() {
 	$page = bp_core_do_network_admin() ? 'admin.php' : 'tools.php' ;
 	$url  = add_query_arg( $query_arg, bp_get_admin_url( $page ) );
 	?>
-	<div class="card tool-box">
+	<div class="card tool-box bp-tools">
 		<h2><?php esc_html_e( 'BuddyPress Tools', 'buddypress' ) ?></h2>
-		<p>
-			<?php esc_html_e( 'BuddyPress keeps track of various relationships between users, groups, and activity items. Occasionally these relationships become out of sync, most often after an import, update, or migration.', 'buddypress' ); ?>
-			<?php
-			printf(
-				/* translators: %s: the link to the BuddyPress repair tools */
-				esc_html_x( 'Use the %s to repair these relationships.', 'buddypress tools intro', 'buddypress' ),
-				'<a href="' . esc_url( $url ) . '">' . esc_html__( 'BuddyPress Tools', 'buddypress' ) . '</a>'
-			);
-			?>
-		</p>
+
+		<dl>
+			<dt><?php esc_html_e( 'Repair Tools', 'buddypress' ) ?></dt>
+			<dd>
+				<?php esc_html_e( 'BuddyPress keeps track of various relationships between users, groups, and activity items. Occasionally these relationships become out of sync, most often after an import, update, or migration.', 'buddypress' ); ?>
+				<?php
+				printf(
+					/* translators: %s: the link to the BuddyPress repair tools */
+					esc_html_x( 'Use the %s to repair these relationships.', 'buddypress tools intro', 'buddypress' ),
+					'<a href="' . esc_url( $url ) . '">' . esc_html__( 'BuddyPress Repair Tools', 'buddypress' ) . '</a>'
+				);
+				?>
+			</dd>
+			<dt><?php esc_html_e( 'Manage Opt-outs', 'buddypress' ) ?></dt>
+			<dd>
+				<?php esc_html_e( 'BuddyPress stores opt-out requests from people who are not members of this site, but have been contacted via communication from this site, and wish to opt-out from future communication.', 'buddypress' ); ?>
+				<?php
+				$url = add_query_arg( 'page', 'bp-optouts', bp_get_admin_url( $page ) );
+				printf(
+					/* translators: %s: the link to the BuddyPress Nonmember Opt-outs */
+					esc_html_x( 'Visit %s to manage your site&rsquo;s opt-out requests.', 'buddypress opt-outs intro', 'buddypress' ),
+					'<a href="' . esc_url( $url ) . '">' . esc_html__( 'Nonmember Opt-outs', 'buddypress' ) . '</a>'
+				);
+				?>
+			</dd>
+		</dl>
 	</div>
 	<?php
 }
diff --git src/bp-core/admin/css/common.css src/bp-core/admin/css/common.css
index c0af4884c..631305fba 100644
--- src/bp-core/admin/css/common.css
+++ src/bp-core/admin/css/common.css
@@ -306,6 +306,17 @@ TABLE OF CONTENTS:
 	content: "";
 }
 
+body.tools-php .bp-tools dt {
+    font-size: 1.1em;
+    color: #646970;
+    font-weight: 600;
+    margin: 1em 0 0.5em 0;
+}
+
+body.tools-php .bp-tools dd {
+    margin: 0;
+}
+
 /*
  * 2.4 Tooltips
  */
diff --git src/bp-core/bp-core-cache.php src/bp-core/bp-core-cache.php
index 12c674241..35d381a50 100644
--- src/bp-core/bp-core-cache.php
+++ src/bp-core/bp-core-cache.php
@@ -415,3 +415,14 @@ function bp_clear_object_type_terms_cache( $type_id = 0, $taxonomy = '' ) {
 add_action( 'bp_type_inserted', 'bp_clear_object_type_terms_cache' );
 add_action( 'bp_type_updated', 'bp_clear_object_type_terms_cache' );
 add_action( 'bp_type_deleted', 'bp_clear_object_type_terms_cache' );
+
+/**
+ * Resets all incremented bp_optout caches.
+ *
+ * @since 8.0.0
+ */
+function bp_optouts_reset_cache_incrementor() {
+	bp_core_reset_incrementor( 'bp_optouts' );
+}
+add_action( 'bp_optout_after_save', 'bp_optouts_reset_cache_incrementor' );
+add_action( 'bp_optout_after_delete', 'bp_optouts_reset_cache_incrementor' );
diff --git src/bp-core/bp-core-functions.php src/bp-core/bp-core-functions.php
index 5ae2a54e5..afb941bf8 100644
--- src/bp-core/bp-core-functions.php
+++ src/bp-core/bp-core-functions.php
@@ -4276,3 +4276,86 @@ function bp_get_widget_max_count_limit( $widget_class = '' ) {
 	 */
 	return apply_filters( 'bp_get_widget_max_count_limit', 50, $widget_class );
 }
+
+/**
+ * Add a new BP_Optout.
+ *
+ * @since 8.0.0
+ *
+ * @param array $args {
+ *     An array of arguments describing the new opt-out.
+ *     @type string $email_address Email address of user who has opted out.
+ *     @type int    $user_id       Optional. ID of user whose communication
+ *                                 prompted the user to opt-out.
+ *     @type string $email_type    Optional. Name of the email type that
+ *                                 prompted the user to opt-out.
+ *     @type string $date_modified Optional. Specify a time, else now will be used.
+ * }
+ * @return false|int False on failure, ID of new (or existing) opt-out if successful.
+ */
+function bp_add_optout( $args = array() ) {
+	$optout = new BP_Optout();
+	$r      = bp_parse_args(
+		$args, array(
+			'email_address' => '',
+			'user_id'       => 0,
+			'email_type'    => '',
+			'date_modified' => bp_core_current_time(),
+		),
+		'add_optout'
+	);
+
+	// Opt-outs must have an email address.
+	if ( empty( $r['email_address'] ) ) {
+		return false;
+	}
+
+	// Avoid creating duplicate opt-outs.
+	$optout_id = $optout->optout_exists(
+		array(
+			'email_address' => $r['email_address'],
+			'user_id'       => $r['user_id'],
+			'email_type'    => $r['email_type'],
+		)
+	);
+
+	if ( ! $optout_id ) {
+		// Set up the new opt-out.
+		$optout->email_address = $r['email_address'];
+		$optout->user_id       = $r['user_id'];
+		$optout->email_type    = $r['email_type'];
+		$optout->date_modified = $r['date_modified'];
+
+		$optout_id = $optout->save();
+	}
+
+	return $optout_id;
+}
+
+/**
+ * Find matching BP_Optouts.
+ *
+ * @since 8.0.0
+ *
+ * @see BP_Optout::get() for a description of parameters and return values.
+ *
+ * @param array $args See {@link BP_Optout::get()}.
+ * @return array See {@link BP_Optout::get()}.
+ */
+function bp_get_optouts( $args = array() ) {
+	$optout_class = new BP_Optout();
+	return $optout_class::get( $args );
+}
+
+/**
+ * Delete a BP_Optout by ID.
+ *
+ * @since 8.0.0
+ *
+ * @param int $id ID of the optout to delete.
+ * @return bool True on success, false on failure.
+ */
+function bp_delete_optout_by_id( $id = 0 ) {
+	$optout_class = new BP_Optout();
+	return $optout_class::delete_by_id( $id );
+}
diff --git src/bp-core/bp-core-update.php src/bp-core/bp-core-update.php
index 7cb66c837..1556f6146 100644
--- src/bp-core/bp-core-update.php
+++ src/bp-core/bp-core-update.php
@@ -651,6 +651,8 @@ function bp_update_to_8_0() {
 			}
 		}
 	}
+
+	bp_core_install_nonmember_opt_outs();
 }
 
 /**
diff --git src/bp-core/classes/class-bp-admin.php src/bp-core/classes/class-bp-admin.php
index 617c7d90a..1e10deab7 100644
--- src/bp-core/classes/class-bp-admin.php
+++ src/bp-core/classes/class-bp-admin.php
@@ -123,6 +123,7 @@ class BP_Admin {
 		require( $this->admin_dir . 'bp-core-admin-components.php' );
 		require( $this->admin_dir . 'bp-core-admin-slugs.php'      );
 		require( $this->admin_dir . 'bp-core-admin-tools.php'      );
+		require( $this->admin_dir . 'bp-core-admin-optouts.php'    );
 	}
 
 	/**
@@ -301,6 +302,15 @@ class BP_Admin {
 			'bp_core_admin_tools'
 		);
 
+		$hooks[] = add_submenu_page(
+			$tools_parent,
+			__( 'Manage Opt-outs', 'buddypress' ),
+			__( 'Manage Opt-outs', 'buddypress' ),
+			$this->capability,
+			'bp-optouts',
+			'bp_core_optouts_admin'
+		);
+
 		// For network-wide configs, add a link to (the root site's) Emails screen.
 		if ( is_network_admin() && bp_is_network_activated() ) {
 			$email_labels = bp_get_email_post_type_labels();
@@ -523,6 +533,13 @@ class BP_Admin {
 		// About and Credits pages.
 		remove_submenu_page( 'index.php', 'bp-about'   );
 		remove_submenu_page( 'index.php', 'bp-credits' );
+
+		// Nonmembers Opt-outs page.
+		if ( is_network_admin() ) {
+			remove_submenu_page( 'network-tools', 'bp-optouts' );
+		} else {
+			remove_submenu_page( 'tools.php', 'bp-optouts' );
+		}
 	}
 
 	/**
diff --git src/bp-core/classes/class-bp-optout.php src/bp-core/classes/class-bp-optout.php
new file mode 100644
index 000000000..90dff5f83
--- /dev/null
+++ src/bp-core/classes/class-bp-optout.php
@@ -0,0 +1,824 @@
+<?php
+/**
+ * BuddyPress Nonmember Opt-out Class
+ *
+ * @package BuddyPress
+ * @subpackage Nonmember Opt-outs
+ *
+ * @since 8.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * BuddyPress opt-outs.
+ *
+ * Use this class to create, access, edit, or delete BuddyPress Nonmember Opt-outs.
+ *
+ * @since 8.0.0
+ */
+class BP_Optout {
+
+	/**
+	 * The opt-out ID.
+	 *
+	 * @since 8.0.0
+	 * @access public
+	 * @var int
+	 */
+	public $id;
+
+	/**
+	 * The hashed email address of the user that wishes to opt out of
+	 * communications from this site.
+	 *
+	 * @since 8.0.0
+	 * @access public
+	 * @var string
+	 */
+	public $email_address;
+
+	/**
+	 * The ID of the user that generated the contact that resulted in the opt-out.
+	 *
+	 * @since 8.0.0
+	 * @access public
+	 * @var int
+	 */
+	public $user_id;
+
+	/**
+	 * The type of email contact that resulted in the opt-out.
+	 * This should be one of the known BP_Email types.
+	 *
+	 * @since 8.0.0
+	 * @access public
+	 * @var string
+	 */
+	public $email_type;
+
+	/**
+	 * The date the opt-out was last modified.
+	 *
+	 * @since 8.0.0
+	 * @access public
+	 * @var string
+	 */
+	public $date_modified;
+
+	/** Public Methods ****************************************************/
+
+	/**
+	 * Constructor method.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @param int $id Optional. Provide an ID to access an existing
+	 *        optout item.
+	 */
+	public function __construct( $id = 0 ) {
+		if ( ! empty( $id ) ) {
+			$this->id = (int) $id;
+			$this->populate();
+		}
+	}
+
+	/**
+	 * Get the opt-outs table name.
+	 *
+	 * @since 8.0.0
+	 * @access public
+	 * @return string
+	 */
+	public static function get_table_name() {
+		return buddypress()->members->table_name_optouts;
+	}
+
+	/**
+	 * Update or insert opt-out details into the database.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @global wpdb $wpdb WordPress database object.
+	 *
+	 * @return bool True on success, false on failure.
+	 */
+	public function save() {
+
+		// Return value
+		$retval = false;
+
+		// Default data and format
+		$data = array(
+			'email_address_hash' => $this->email_address,
+			'user_id'            => $this->user_id,
+			'email_type'         => sanitize_key( $this->email_type ),
+			'date_modified'      => $this->date_modified,
+		);
+		$data_format = array( '%s', '%d', '%s', '%s' );
+
+		/**
+		 * Fires before an opt-out is saved.
+		 *
+		 * @since 8.0.0
+		 *
+		 * @param BP_Optout object $this Characteristics of the opt-out to be saved.
+		 */
+		do_action_ref_array( 'bp_optout_before_save', array( &$this ) );
+
+		// Update.
+		if ( ! empty( $this->id ) ) {
+			$result = self::_update( $data, array( 'ID' => $this->id ), $data_format, array( '%d' ) );
+		// Insert.
+		} else {
+			$result = self::_insert( $data, $data_format );
+		}
+
+		// Set the opt-out ID if successful.
+		if ( ! empty( $result ) && ! is_wp_error( $result ) ) {
+			global $wpdb;
+
+			$this->id = $wpdb->insert_id;
+			$retval   = $wpdb->insert_id;
+		}
+
+		wp_cache_delete( $this->id, 'bp_optouts' );
+
+		/**
+		 * Fires after an optout is saved.
+		 *
+		 * @since 8.0.0
+		 *
+		 * @param BP_optout object $this Characteristics of the opt-out just saved.
+		 */
+		do_action_ref_array( 'bp_optout_after_save', array( &$this ) );
+
+		// Return the result.
+		return $retval;
+	}
+
+	/**
+	 * Fetch data for an existing opt-out from the database.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @global BuddyPress $bp The one true BuddyPress instance.
+	 * @global wpdb $wpdb WordPress database object.
+	 */
+	public function populate() {
+		global $wpdb;
+		$optouts_table_name = $this->get_table_name();
+
+		// Check cache for optout data.
+		$optout = wp_cache_get( $this->id, 'bp_optouts' );
+
+		// Cache missed, so query the DB.
+		if ( false === $optout ) {
+			$optout = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$optouts_table_name} WHERE id = %d", $this->id ) );
+			wp_cache_set( $this->id, $optout, 'bp_optouts' );
+		}
+
+		// No optout found so set the ID and bail.
+		if ( empty( $optout ) || is_wp_error( $optout ) ) {
+			$this->id = 0;
+			return;
+		}
+
+		$this->email_address = $optout->email_address_hash;
+		$this->user_id       = (int) $optout->user_id;
+		$this->email_type    = sanitize_key( $optout->email_type );
+		$this->date_modified = $optout->date_modified;
+
+	}
+
+	/** Protected Static Methods ******************************************/
+
+	/**
+	 * Create an opt-out entry.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @param array $data {
+	 *     Array of optout data, passed to {@link wpdb::insert()}.
+	 *	   @type string $email_address     The hashed email address of the user that wishes to opt out of
+	 *                                     communications from this site.
+	 *	   @type int    $user_id           The ID of the user that generated the contact that resulted in the opt-out.
+	 * 	   @type string $email_type        The type of email contact that resulted in the opt-out.
+	 * 	   @type string $date_modified     Date the opt-out was last modified.
+	 * }
+	 * @param array $data_format See {@link wpdb::insert()}.
+	 * @return int|false The number of rows inserted, or false on error.
+	 */
+	protected static function _insert( $data = array(), $data_format = array() ) {
+		global $wpdb;
+		// We must hash the email address at insert.
+		$data['email_address_hash'] = wp_hash( $data['email_address_hash'] );
+		return $wpdb->insert( BP_Optout::get_table_name(), $data, $data_format );
+	}
+
+	/**
+	 * Update opt-outs.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @see wpdb::update() for further description of paramater formats.
+	 *
+	 * @param array $data         Array of opt-out data to update, passed to
+	 *                            {@link wpdb::update()}. Accepts any property of a
+	 *                            BP_optout object.
+	 * @param array $where        The WHERE params as passed to wpdb::update().
+	 *                            Typically consists of array( 'ID' => $id ) to specify the ID
+	 *                            of the item being updated. See {@link wpdb::update()}.
+	 * @param array $data_format  See {@link wpdb::insert()}.
+	 * @param array $where_format See {@link wpdb::insert()}.
+	 * @return int|false The number of rows updated, or false on error.
+	 */
+	protected static function _update( $data = array(), $where = array(), $data_format = array(), $where_format = array() ) {
+		global $wpdb;
+
+		// Ensure that a passed email address is hashed.
+		if ( ! empty( $data['email_address_hash'] ) && is_email( $data['email_address_hash'] ) ) {
+			$data['email_address_hash'] = wp_hash( $data['email_address_hash'] );
+		}
+
+		return $wpdb->update( BP_Optout::get_table_name(), $data, $where, $data_format, $where_format );
+	}
+
+	/**
+	 * Delete opt-outs.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @see wpdb::update() for further description of paramater formats.
+	 *
+	 * @param array $where        Array of WHERE clauses to filter by, passed to
+	 *                            {@link wpdb::delete()}. Accepts any property of a
+	 *                            BP_optout object.
+	 * @param array $where_format See {@link wpdb::insert()}.
+	 * @return int|false The number of rows updated, or false on error.
+	 */
+	protected static function _delete( $where = array(), $where_format = array() ) {
+		global $wpdb;
+		return $wpdb->delete( BP_Optout::get_table_name(), $where, $where_format );
+	}
+
+	/**
+	 * Assemble the WHERE clause of a get() SQL statement.
+	 *
+	 * Used by BP_optout::get() to create its WHERE
+	 * clause.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @param array $args See {@link BP_optout::get()} for more details.
+	 * @return string WHERE clause.
+	 */
+	protected static function get_where_sql( $args = array() ) {
+		global $wpdb;
+
+		$where_conditions = array();
+		$where            = '';
+
+		// id.
+		if ( false !== $args['id'] ) {
+			$id_in                  = implode( ',', wp_parse_id_list( $args['id'] ) );
+			$where_conditions['id'] = "id IN ({$id_in})";
+		}
+
+		// email_address.
+		if ( ! empty( $args['email_address'] ) ) {
+			if ( ! is_array( $args['email_address'] ) ) {
+				$emails = explode( ',', $args['email_address'] );
+			} else {
+				$emails = $args['email_address'];
+			}
+
+			$email_clean = array();
+			foreach ( $emails as $email ) {
+				$email_hash    = wp_hash( $email );
+				$email_clean[] = $wpdb->prepare( '%s', $email_hash );
+			}
+
+			$email_in                          = implode( ',', $email_clean );
+			$where_conditions['email_address'] = "email_address_hash IN ({$email_in})";
+		}
+
+		// user_id.
+		if ( ! empty( $args['user_id'] ) ) {
+			$user_id_in                  = implode( ',', wp_parse_id_list( $args['user_id'] ) );
+			$where_conditions['user_id'] = "user_id IN ({$user_id_in})";
+		}
+
+		// email_type.
+		if ( ! empty( $args['email_type'] ) ) {
+			if ( ! is_array( $args['email_type'] ) ) {
+				$email_types = explode( ',', $args['email_type'] );
+			} else {
+				$email_types = $args['email_type'];
+			}
+
+			$et_clean = array();
+			foreach ( $email_types as $et ) {
+				$et_clean[] = $wpdb->prepare( '%s', sanitize_key( $et ) );
+			}
+
+			$et_in                          = implode( ',', $et_clean );
+			$where_conditions['email_type'] = "email_type IN ({$et_in})";
+		}
+
+		// search_terms.
+		if ( ! empty( $args['search_terms'] ) ) {
+			// Matching email_address is an exact match because of the hashing.
+			$search_terms_like                = wp_hash( $args['search_terms'] );
+			$where_conditions['search_terms'] = $wpdb->prepare( '( email_address_hash LIKE %s )', $search_terms_like );
+		}
+
+		// Custom WHERE.
+		if ( ! empty( $where_conditions ) ) {
+			$where = 'WHERE ' . implode( ' AND ', $where_conditions );
+		}
+
+		return $where;
+	}
+
+	/**
+	 * Assemble the ORDER BY clause of a get() SQL statement.
+	 *
+	 * Used by BP_Optout::get() to create its ORDER BY
+	 * clause.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @param array $args See {@link BP_optout::get()} for more details.
+	 * @return string ORDER BY clause.
+	 */
+	protected static function get_order_by_sql( $args = array() ) {
+
+		$conditions = array();
+		$retval     = '';
+
+		// Order by.
+		if ( ! empty( $args['order_by'] ) ) {
+			$order_by_clean = array();
+			$columns        = array( 'id', 'email_address_hash', 'user_id', 'email_type', 'date_modified' );
+			foreach ( (array) $args['order_by'] as $key => $value ) {
+				if ( in_array( $value, $columns, true ) ) {
+					$order_by_clean[] = $value;
+				}
+			}
+			if ( ! empty( $order_by_clean ) ) {
+				$order_by               = implode( ', ', $order_by_clean );
+				$conditions['order_by'] = "{$order_by}";
+			}
+		}
+
+		// Sort order direction.
+		if ( ! empty( $args['sort_order'] ) ) {
+			$sort_order               = bp_esc_sql_order( $args['sort_order'] );
+			$conditions['sort_order'] = "{$sort_order}";
+		}
+
+		// Custom ORDER BY.
+		if ( ! empty( $conditions['order_by'] ) ) {
+			$retval = 'ORDER BY ' . implode( ' ', $conditions );
+		}
+
+		return $retval;
+	}
+
+	/**
+	 * Assemble the LIMIT clause of a get() SQL statement.
+	 *
+	 * Used by BP_Optout::get() to create its LIMIT clause.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @param array $args See {@link BP_optout::get()} for more details.
+	 * @return string LIMIT clause.
+	 */
+	protected static function get_paged_sql( $args = array() ) {
+		global $wpdb;
+
+		// Setup local variable.
+		$retval = '';
+
+		// Custom LIMIT.
+		if ( ! empty( $args['page'] ) && ! empty( $args['per_page'] ) ) {
+			$page     = absint( $args['page']     );
+			$per_page = absint( $args['per_page'] );
+			$offset   = $per_page * ( $page - 1 );
+			$retval   = $wpdb->prepare( "LIMIT %d, %d", $offset, $per_page );
+		}
+
+		return $retval;
+	}
+
+	/**
+	 * Assemble query clauses, based on arguments, to pass to $wpdb methods.
+	 *
+	 * The insert(), update(), and delete() methods of {@link wpdb} expect
+	 * arguments of the following forms:
+	 *
+	 * - associative arrays whose key/value pairs are column => value, to
+	 *   be used in WHERE, SET, or VALUES clauses
+	 * - arrays of "formats", which tell $wpdb->prepare() which type of
+	 *   value to expect when sanitizing (eg, array( '%s', '%d' ))
+	 *
+	 * This utility method can be used to assemble both kinds of params,
+	 * out of a single set of associative array arguments, such as:
+	 *
+	 *     $args = array(
+	 *         'user_id'    => 4,
+	 *         'email_type' => 'type_string',
+	 *     );
+	 *
+	 * This will be converted to:
+	 *
+	 *     array(
+	 *         'data' => array(
+	 *             'user_id' => 4,
+	 *             'email_type'   => 'type_string',
+	 *         ),
+	 *         'format' => array(
+	 *             '%d',
+	 *             '%s',
+	 *         ),
+	 *     )
+	 *
+	 * which can easily be passed as arguments to the $wpdb methods.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @param array $args Associative array of filter arguments.
+	 *                    See {@BP_optout::get()} for a breakdown.
+	 * @return array Associative array of 'data' and 'format' args.
+	 */
+	protected static function get_query_clauses( $args = array() ) {
+		$where_clauses = array(
+			'data'   => array(),
+			'format' => array(),
+		);
+
+		// id.
+		if ( ! empty( $args['id'] ) ) {
+			$where_clauses['data']['id'] = absint( $args['id'] );
+			$where_clauses['format'][]   = '%d';
+		}
+
+		// email_address.
+		if ( ! empty( $args['email_address'] ) ) {
+			$where_clauses['data']['email_address_hash'] = $args['email_address'];
+			$where_clauses['format'][]                   = '%s';
+		}
+
+		// user_id.
+		if ( ! empty( $args['user_id'] ) ) {
+			$where_clauses['data']['user_id'] = absint( $args['user_id'] );
+			$where_clauses['format'][]        = '%d';
+		}
+
+		// email_type.
+		if ( ! empty( $args['email_type'] ) ) {
+			$where_clauses['data']['email_type'] = $args['email_type'];
+			$where_clauses['format'][]           = '%s';
+		}
+
+		return $where_clauses;
+	}
+
+	/** Public Static Methods *********************************************/
+
+	/**
+	 * Get opt-outs, based on provided filter parameters.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @param array $args {
+	 *     Associative array of arguments. All arguments but $page and
+	 *     $per_page can be treated as filter values for get_where_sql()
+	 *     and get_query_clauses(). All items are optional.
+	 *     @type int|array    $id                ID of opt-out.
+	 *                                           Can be an array of IDs.
+	 *     @type string|array $email_address     Email address of users who have opted out
+	 *			                                 being queried. Can be an array of addresses.
+	 *     @type int|array    $user_id           ID of user whose communication prompted the
+	 *                                           opt-out. Can be an array of IDs.
+	 *     @type string|array $email_type        Name of the emil type to filter by.
+	 *                                           Can be an array of email types.
+	 *     @type string       $search_terms      Term to match against email_address field.
+	 *     @type string       $order_by          Database column to order by.
+	 *     @type string       $sort_order        Either 'ASC' or 'DESC'.
+	 *     @type int          $page              Number of the current page of results.
+	 *                                           Default: false (no pagination,
+	 *                                           all items).
+	 *     @type int          $per_page          Number of items to show per page.
+	 *                                           Default: false (no pagination,
+	 *                                           all items).
+  	 *     @type string       $fields            Which fields to return. Specify 'email_addresses' to
+  	 *                                           fetch a list of opt-out email_addresses.
+  	 *                                           Specify 'user_ids' to
+  	 *                                           fetch a list of opt-out user_ids.
+  	 *                                           Specify 'ids' to fetch a list of opt-out IDs.
+ 	 *                                           Default: 'all' (return BP_Optout objects).
+	 * }
+	 *
+	 * @return array BP_Optout objects | IDs of found opt-outs | Email addresses of matches.
+	 */
+	public static function get( $args = array() ) {
+		global $wpdb;
+		$optouts_table_name = BP_Optout::get_table_name();
+
+		// Parse the arguments.
+		$r = bp_parse_args(
+			$args,
+			array(
+				'id'            => false,
+				'email_address' => false,
+				'user_id'       => false,
+				'email_type'    => false,
+				'search_terms'  => '',
+				'order_by'      => false,
+				'sort_order'    => false,
+				'page'          => false,
+				'per_page'      => false,
+				'fields'        => 'all',
+			),
+			'bp_optout_get'
+		);
+
+		$sql = array(
+			'select'     => "SELECT",
+			'fields'     => '',
+			'from'       => "FROM {$optouts_table_name} o",
+			'where'      => '',
+			'orderby'    => '',
+			'pagination' => '',
+		);
+
+		if ( 'user_ids' === $r['fields'] ) {
+			$sql['fields'] = "DISTINCT o.user_id";
+		} else if ( 'email_addresses' === $r['fields'] ) {
+			$sql['fields'] = "DISTINCT o.email_address_hash";
+		} else {
+			$sql['fields'] = 'DISTINCT o.id';
+		}
+
+		// WHERE.
+		$sql['where'] = self::get_where_sql(
+			array(
+				'id'            => $r['id'],
+				'email_address' => $r['email_address'],
+				'user_id'       => $r['user_id'],
+				'email_type'    => $r['email_type'],
+				'search_terms'  => $r['search_terms'],
+			)
+		);
+
+		// ORDER BY.
+		$sql['orderby'] = self::get_order_by_sql(
+			array(
+				'order_by'   => $r['order_by'],
+				'sort_order' => $r['sort_order']
+			)
+		);
+
+		// LIMIT %d, %d.
+		$sql['pagination'] = self::get_paged_sql(
+			array(
+				'page'     => $r['page'],
+				'per_page' => $r['per_page'],
+			)
+		);
+
+		$paged_optouts_sql = "{$sql['select']} {$sql['fields']} {$sql['from']} {$sql['where']} {$sql['orderby']} {$sql['pagination']}";
+
+		/**
+		 * Filters the pagination SQL statement.
+		 *
+		 * @since 8.0.0
+		 *
+		 * @param string $value Concatenated SQL statement.
+		 * @param array  $sql   Array of SQL parts before concatenation.
+		 * @param array  $r     Array of parsed arguments for the get method.
+		 */
+		$paged_optouts_sql = apply_filters( 'bp_optouts_get_paged_optouts_sql', $paged_optouts_sql, $sql, $r );
+
+		$cached = bp_core_get_incremented_cache( $paged_optouts_sql, 'bp_optouts' );
+		if ( false === $cached ) {
+			$paged_optout_ids = $wpdb->get_col( $paged_optouts_sql );
+			bp_core_set_incremented_cache( $paged_optouts_sql, 'bp_optouts', $paged_optout_ids );
+		} else {
+			$paged_optout_ids = $cached;
+		}
+
+		// Special return format cases.
+		if ( in_array( $r['fields'], array( 'ids', 'user_ids' ), true ) ) {
+			// We only want the field that was found.
+			return array_map( 'intval', $paged_optout_ids );
+		} else if ( 'email_addresses' === $r['fields'] ) {
+			return $paged_optout_ids;
+		}
+
+		$uncached_ids = bp_get_non_cached_ids( $paged_optout_ids, 'bp_optouts' );
+		if ( $uncached_ids ) {
+			$ids_sql = implode( ',', array_map( 'intval', $uncached_ids ) );
+			$data_objects = $wpdb->get_results( "SELECT o.* FROM {$optouts_table_name} o WHERE o.id IN ({$ids_sql})" );
+			foreach ( $data_objects as $data_object ) {
+				wp_cache_set( $data_object->id, $data_object, 'bp_optouts' );
+			}
+		}
+
+		$paged_optouts = array();
+		foreach ( $paged_optout_ids as $paged_optout_id ) {
+			$paged_optouts[] = new BP_optout( $paged_optout_id );
+		}
+
+		return $paged_optouts;
+	}
+
+	/**
+	 * Get a count of total optouts matching a set of arguments.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @see BP_optout::get() for a description of
+	 *      arguments.
+	 *
+	 * @param array $args See {@link BP_optout::get()}.
+	 * @return int Count of located items.
+	 */
+	public static function get_total_count( $args ) {
+		global $wpdb;
+		$optouts_table_name = BP_Optout::get_table_name();
+
+		// Parse the arguments.
+		$r  = bp_parse_args(
+			$args,
+			array(
+				'id'            => false,
+				'email_address' => false,
+				'user_id'       => false,
+				'email_type'    => false,
+				'search_terms'  => '',
+				'order_by'      => false,
+				'sort_order'    => false,
+				'page'          => false,
+				'per_page'      => false,
+				'fields'        => 'all',
+			),
+			'bp_optout_get_total_count'
+		);
+
+		// Build the query
+		$select_sql = "SELECT COUNT(*)";
+		$from_sql   = "FROM {$optouts_table_name}";
+		$where_sql  = self::get_where_sql( $r );
+		$sql        = "{$select_sql} {$from_sql} {$where_sql}";
+
+		// Return the queried results
+		return $wpdb->get_var( $sql );
+	}
+
+	/**
+	 * Update optouts.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @see BP_optout::get() for a description of
+	 *      accepted update/where arguments.
+	 *
+	 * @param array $update_args Associative array of fields to update,
+	 *                           and the values to update them to. Of the format
+	 *                           array( 'user_id' => 4, 'email_address' => 'bar@foo.com', ).
+	 * @param array $where_args  Associative array of columns/values, to
+	 *                           determine which rows should be updated. Of the format
+	 *                           array( 'user_id' => 7, 'email_address' => 'bar@foo.com', ).
+	 * @return int|bool Number of rows updated on success, false on failure.
+	 */
+	public static function update( $update_args = array(), $where_args = array() ) {
+		$update = self::get_query_clauses( $update_args );
+		$where  = self::get_query_clauses( $where_args  );
+
+		/**
+		 * Fires before an opt-out is updated.
+		 *
+		 * @since 8.0.0
+		 *
+		 * @param array $where_args  Associative array of columns/values describing
+		 *                           opt-outs about to be deleted.
+		 * @param array $update_args Array of new values.
+		 */
+		do_action( 'bp_optout_before_update', $where_args, $update_args );
+
+		$retval = self::_update( $update['data'], $where['data'], $update['format'], $where['format'] );
+
+		// Clear matching items from the cache.
+		$cache_args           = $where_args;
+		$cache_args['fields'] = 'ids';
+		$maybe_cached_ids     = self::get( $cache_args );
+		foreach ( $maybe_cached_ids as $invite_id ) {
+			wp_cache_delete( $invite_id, 'bp_optouts' );
+		}
+
+		/**
+		 * Fires after an opt-out is updated.
+		 *
+		 * @since 8.0.0
+		 *
+		 * @param array $where_args  Associative array of columns/values describing
+		 *                           opt-outs about to be deleted.
+		 * @param array $update_args Array of new values.
+		 */
+		do_action( 'bp_optout_after_update', $where_args, $update_args );
+
+  		return $retval;
+	}
+
+	/**
+	 * Delete opt-outs.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @see BP_optout::get() for a description of
+	 *      accepted where arguments.
+	 *
+	 * @param array $args Associative array of columns/values, to determine
+	 *                    which rows should be deleted.  Of the format
+	 *                    array( 'user_id' => 7, 'email_address' => 'bar@foo.com', ).
+	 * @return int|bool Number of rows deleted on success, false on failure.
+	 */
+	public static function delete( $args = array() ) {
+		$where = self::get_query_clauses( $args );
+
+		/**
+		 * Fires before an opt-out is deleted.
+		 *
+		 * @since 8.0.0
+		 *
+		 * @param array $args Characteristics of the opt-outs to be deleted.
+		 */
+		do_action( 'bp_optout_before_delete', $args );
+
+		// Clear matching items from the cache.
+		$cache_args           = $args;
+		$cache_args['fields'] = 'ids';
+		$maybe_cached_ids     = self::get( $cache_args );
+		foreach ( $maybe_cached_ids as $invite_id ) {
+			wp_cache_delete( $invite_id, 'bp_optouts' );
+		}
+
+		$retval = self::_delete( $where['data'], $where['format'] );
+
+		/**
+		 * Fires after an opt-out is deleted.
+		 *
+		 * @since 8.0.0
+		 *
+		 * @param array $args Characteristics of the opt-outs just deleted.
+		 */
+		do_action( 'bp_optout_after_delete', $args );
+
+		return $retval;
+	}
+
+	/** Convenience methods ***********************************************/
+
+	/**
+	 * Check whether an invitation exists matching the passed arguments.
+	 *
+	 * @since 5.0.0
+	 *
+	 * @see BP_Optout::get() for a description of accepted parameters.
+	 *
+	 * @return int|bool ID of first found invitation or false if none found.
+	 */
+	public function optout_exists( $args = array() ) {
+		$exists = false;
+
+		$args['fields'] = 'ids';
+		$optouts        = BP_Optout::get( $args );
+		if ( $optouts ) {
+			$exists = current( $optouts );
+		}
+
+		return $exists;
+	}
+
+	/**
+	 * Delete a single opt-out by ID.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @see BP_optout::delete() for explanation of
+	 *      return value.
+	 *
+	 * @param int $id ID of the opt-out item to be deleted.
+	 * @return bool True on success, false on failure.
+	 */
+	public static function delete_by_id( $id ) {
+		return self::delete( array(
+			'id' => $id,
+		) );
+	}
+}
diff --git src/bp-core/classes/class-bp-optouts-list-table.php src/bp-core/classes/class-bp-optouts-list-table.php
new file mode 100644
index 000000000..0e9084245
--- /dev/null
+++ src/bp-core/classes/class-bp-optouts-list-table.php
@@ -0,0 +1,393 @@
+<?php
+/**
+ * BuddyPress Opt-outs List Table class.
+ *
+ * @package BuddyPress
+ * @subpackage CoreAdminClasses
+ * @since 8.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * List table class for nonmember opt-outs admin page.
+ *
+ * @since 8.0.0
+ */
+class BP_Optouts_List_Table extends WP_Users_List_Table {
+
+	/**
+	 * Opt-out count.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @var int
+	 */
+	public $total_items = 0;
+
+	/**
+	 * Constructor.
+	 *
+	 * @since 8.0.0
+	 */
+	public function __construct() {
+		// Define singular and plural labels, as well as whether we support AJAX.
+		parent::__construct(
+			array(
+				'ajax'     => false,
+				'plural'   => 'optouts',
+				'singular' => 'optout',
+				'screen'   => get_current_screen()->id,
+			)
+		);
+	}
+
+	/**
+	 * Set up items for display in the list table.
+	 *
+	 * Handles filtering of data, sorting, pagination, and any other data
+	 * manipulation required prior to rendering.
+	 *
+	 * @since 8.0.0
+	 */
+	public function prepare_items() {
+		global $usersearch;
+
+		$search   = isset( $_REQUEST['s'] ) ? $_REQUEST['s'] : '';
+		$per_page = $this->get_items_per_page( str_replace( '-', '_', "{$this->screen->id}_per_page" ) );
+		$paged    = $this->get_pagenum();
+
+		$args = array(
+			'search_terms' => $search,
+			'order_by'     => 'date_modified',
+			'sort_order'   => 'DESC',
+			'page'         => $paged,
+			'per_page'     => $per_page,
+		);
+
+		if ( isset( $_REQUEST['orderby'] ) ) {
+			$args['order_by'] = $_REQUEST['orderby'];
+		}
+
+		if ( isset( $_REQUEST['order'] ) ) {
+			$args['sort_order'] = $_REQUEST['order'];
+		}
+
+		$this->items       = bp_get_optouts( $args );
+		$optouts_class     = new BP_Optout();
+		$this->total_items = $optouts_class->get_total_count( $args );
+
+		$this->set_pagination_args(
+			array(
+				'total_items' => $this->total_items,
+				'per_page'    => $per_page,
+			)
+		);
+	}
+
+	/**
+	 * Get the list of views available on this table.
+	 *
+	 * @since 8.0.0
+	 */
+	public function views() {
+		if ( is_multisite() && bp_core_do_network_admin() ) {
+			$tools_parent = 'admin.php';
+		} else {
+			$tools_parent = 'tools.php';
+		}
+
+		$url_base = add_query_arg(
+			array(
+				'page' => 'bp-optouts',
+			),
+			bp_get_admin_url( $tools_parent )
+		);
+		?>
+
+		<h2 class="screen-reader-text">
+			<?php
+				/* translators: accessibility text */
+				esc_html_e( 'Filter opt-outs list', 'buddypress' );
+			?>
+		</h2>
+		<ul class="subsubsub">
+			<?php
+			/**
+			 * Fires inside listing of views so plugins can add their own.
+			 *
+			 * @since 8.0.0
+			 *
+			 * @param string $url_base       Current URL base for view.
+			 * @param array  $active_filters Current filters being requested.
+			 */
+			do_action( 'bp_optouts_list_table_get_views', $url_base, $this->active_filters ); ?>
+		</ul>
+	<?php
+	}
+
+	/**
+	 * Get rid of the extra nav.
+	 *
+	 * WP_Users_List_Table will add an extra nav to change user's role.
+	 * As we're dealing with opt-outs, we don't need this.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @param array $which Current table nav item.
+	 */
+	public function extra_tablenav( $which ) {
+		return;
+	}
+
+	/**
+	 * Specific opt-out columns.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @return array
+	 */
+	public function get_columns() {
+		/**
+		 * Filters the nonmember opt-outs columns.
+		 *
+		 * @since 8.0.0
+		 *
+		 * @param array $value Array of columns to display.
+		 */
+		return apply_filters(
+			'bp_optouts_list_columns',
+			array(
+				'cb'                     => '<input type="checkbox" />',
+				'email_address'          => __( 'Email Address Hash', 'buddypress' ),
+				'username'               => __( 'Email Sender', 'buddypress' ),
+				'user_registered'        => __( 'Email Sender Registered', 'buddypress' ),
+				'email_type'             => __( 'Email Type', 'buddypress' ),
+				'email_type_description' => __( 'Email Description', 'buddypress' ),
+				'optout_date_modified'   => __( 'Date Modified', 'buddypress' ),
+			)
+		);
+	}
+
+	/**
+	 * Specific bulk actions for opt-outs.
+	 *
+	 * @since 8.0.0
+	 */
+	public function get_bulk_actions() {
+		if ( current_user_can( 'delete_users' ) ) {
+			$actions['delete'] = _x( 'Delete', 'Optout database record action', 'buddypress' );
+		}
+
+		return $actions;
+	}
+
+	/**
+	 * The text shown when no items are found.
+	 *
+	 * Nice job, clean sheet!
+	 *
+	 * @since 8.0.0
+	 */
+	public function no_items() {
+		esc_html_e( 'No opt-outs found.', 'buddypress' );
+	}
+
+	/**
+	 * The columns opt-outs can be reordered by.
+	 *
+	 * @since 8.0.0
+	 */
+	public function get_sortable_columns() {
+		return array(
+			'username'             => 'user_id',
+			'email_type'           => 'email_type',
+			'optout_date_modified' => 'date_modified',
+		);
+	}
+
+	/**
+	 * Display opt-out rows.
+	 *
+	 * @since 8.0.0
+	 */
+	public function display_rows() {
+		$style = '';
+		foreach ( $this->items as $optout ) {
+			$style = ( ' class="alternate"' == $style ) ? '' : ' class="alternate"';
+			echo "\n\t" . $this->single_row( $optout, $style );
+		}
+	}
+
+	/**
+	 * Display an opt-out row.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @see WP_List_Table::single_row() for explanation of params.
+	 *
+	 * @param BP_Optout $optout   BP_Optout object.
+	 * @param string    $style    Styles for the row.
+	 * @param string    $role     Role to be assigned to user.
+	 * @param int       $numposts Number of posts.
+	 * @return void
+	 */
+	public function single_row( $optout = null, $style = '', $role = '', $numposts = 0 ) {
+		echo '<tr' . $style . ' id="optout-' . intval( $optout->id ) . '">';
+		echo $this->single_row_columns( $optout );
+		echo '</tr>';
+	}
+
+	/**
+	 * Markup for the checkbox used to select items for bulk actions.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @param BP_Optout $optout BP_Optout object.
+	 */
+	public function column_cb( $optout = null ) {
+	?>
+		<label class="screen-reader-text" for="optout_<?php echo intval( $optout->id ); ?>">
+			<?php
+				/* translators: %d: accessibility text. */
+				printf( esc_html__( 'Select opt-out request: %d', 'buddypress' ), intval( $optout->id ) );
+			?>
+		</label>
+		<input type="checkbox" id="optout_<?php echo intval( $optout->id ) ?>" name="optout_ids[]" value="<?php echo esc_attr( $optout->id ) ?>" />
+		<?php
+	}
+
+	/**
+	 * Markup for the checkbox used to select items for bulk actions.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @param BP_Optout $optout BP_Optout object.
+	 */
+	public function column_email_address( $optout = null ) {
+		printf( '<a href="mailto:%1$s">%2$s</a>', esc_attr( $optout->email_address ), esc_html( $optout->email_address ) );
+
+		$actions = array();
+
+		if ( is_network_admin() ) {
+			$form_url = network_admin_url( 'admin.php' );
+		} else {
+			$form_url = bp_get_admin_url( 'tools.php' );
+		}
+
+		// Delete link.
+		$delete_link = add_query_arg(
+			array(
+				'page'      => 'bp-optouts',
+				'optout_id' => $optout->id,
+				'action'    => 'delete',
+			),
+			$form_url
+		);
+		$actions['delete'] = sprintf( '<a href="%1$s" class="delete">%2$s</a>', esc_url( $delete_link ), esc_html__( 'Delete', 'buddypress' ) );
+
+		/**
+		 * Filters the row actions for each opt-out in list.
+		 *
+		 * @since 8.0.0
+		 *
+		 * @param array  $actions Array of actions and corresponding links.
+		 * @param object $optout  The BP_Optout.
+		 */
+		$actions = apply_filters( 'bp_optouts_management_row_actions', $actions, $optout );
+
+		echo $this->row_actions( $actions );
+	}
+
+	/**
+	 * The inviter/site member who sent the email that prompted the opt-out.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @param BP_Optout $optout BP_Optout object.
+	 */
+	public function column_username( $optout = null ) {
+		$avatar = get_avatar( $optout->user_id, 32 );
+		$inviter = get_user_by( 'id', $optout->user_id );
+		if ( ! $inviter ) {
+			return;
+		}
+		$user_link = bp_core_get_user_domain( $optout->user_id );
+		echo $avatar . sprintf( '<strong><a href="%1$s" class="edit">%2$s</a></strong><br/>', esc_url( $user_link ), esc_html( $inviter->user_login ) );
+	}
+
+	/**
+	 * Display registration date of user whose communication prompted opt-out.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @param BP_Optout $optout BP_Optout object.
+	 */
+	public function column_user_registered( $optout = null ) {
+		$inviter = get_user_by( 'id', $optout->user_id );
+		if ( ! $inviter ) {
+			return;
+		}
+		echo esc_html( mysql2date( 'Y/m/d g:i:s a', $inviter->user_registered  ) );
+	}
+
+	/**
+	 * Display type of email that prompted opt-out.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @param BP_Optout $optout BP_Optout object.
+	 */
+	public function column_email_type( $optout = null ) {
+		echo esc_html( $optout->email_type );
+	}
+
+	/**
+	 * Display description of bp-email-type that prompted opt-out.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @param BP_Optout $optout BP_Optout object.
+	 */
+	public function column_email_type_description( $optout = null ) {
+		$type_term = get_term_by( 'slug', $optout->email_type, 'bp-email-type' );
+		if ( $type_term ) {
+			echo esc_html( $type_term->description );
+		}
+
+	}
+
+	/**
+	 * Display opt-out date.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @param BP_Optout $optout BP_Optout object.
+	 */
+	public function column_optout_date_modified( $optout = null ) {
+		echo esc_html( mysql2date( 'Y/m/d g:i:s a', $optout->date_modified ) );
+	}
+
+	/**
+	 * Allow plugins to add custom columns.
+	 *
+	 * @since 8.0.0
+	 *
+	 * @param BP_Optout $optout      BP_Optout object.
+	 * @param string    $column_name The column name.
+	 * @return string
+	 */
+	function column_default( $optout = null, $column_name = '' ) {
+
+		/**
+		 * Filters the single site custom columns for plugins.
+		 *
+		 * @since 8.0.0
+		 *
+		 * @param string    $column_name The column name.
+		 * @param BP_Optout $optout      BP_Optout object.
+		 */
+		return apply_filters( 'bp_optouts_management_custom_column', '', $column_name, $optout );
+	}
+}
diff --git src/bp-members/classes/class-bp-members-admin.php src/bp-members/classes/class-bp-members-admin.php
index c999cb923..17f73cb37 100644
--- src/bp-members/classes/class-bp-members-admin.php
+++ src/bp-members/classes/class-bp-members-admin.php
@@ -488,6 +488,7 @@ class BP_Members_Admin {
 				'bp-signups',
 				array( $this, 'signups_admin' )
 			);
+
 		}
 
 		$edit_page         = 'user-edit';
@@ -509,6 +510,8 @@ class BP_Members_Admin {
 			$this->user_page    .= '-network';
 			$this->users_page   .= '-network';
 			$this->signups_page .= '-network';
+
+			$this->members_optouts_page .= '-network';
 		}
 
 		// Setup the screen ID's.
@@ -2564,5 +2567,6 @@ class BP_Members_Admin {
 
 		return $value;
 	}
+
 }
 endif; // End class_exists check.
diff --git src/bp-members/classes/class-bp-members-component.php src/bp-members/classes/class-bp-members-component.php
index 9e6fbcd42..4db47da8d 100644
--- src/bp-members/classes/class-bp-members-component.php
+++ src/bp-members/classes/class-bp-members-component.php
@@ -178,6 +178,7 @@ class BP_Members_Component extends BP_Component {
 			'global_tables'   => array(
 				'table_name_invitations'   => bp_core_get_table_prefix() . 'bp_invitations',
 				'table_name_last_activity' => bp_core_get_table_prefix() . 'bp_activity',
+				'table_name_optouts'       => bp_core_get_table_prefix() . 'bp_optouts',
 				'table_name_signups'       => $wpdb->base_prefix . 'signups', // Signups is a global WordPress table.
 			)
 		);
diff --git src/class-buddypress.php src/class-buddypress.php
index 3dd1d483c..1af41585b 100644
--- src/class-buddypress.php
+++ src/class-buddypress.php
@@ -597,6 +597,8 @@ class BuddyPress {
 			'BP_REST_Components_Endpoint'  => 'core',
 			'BP_REST_Attachments'          => 'core',
 			'BP_Admin_Types'               => 'core',
+			'BP_Optout'                    => 'core',
+			'BP_Optouts_List_Table'        => 'core',
 
 			'BP_Core_Friends_Widget'   => 'friends',
 			'BP_REST_Friends_Endpoint' => 'friends',
diff --git tests/phpunit/testcases/core/optouts.php tests/phpunit/testcases/core/optouts.php
new file mode 100644
index 000000000..7c0d1ecc5
--- /dev/null
+++ tests/phpunit/testcases/core/optouts.php
@@ -0,0 +1,103 @@
+<?php
+/**
+ * @group core
+ * @group optouts
+ */
+ class BP_Tests_Optouts extends BP_UnitTestCase {
+	public function test_bp_optouts_add_optout_vanilla() {
+		$old_current_user = get_current_user_id();
+
+		$u1 = $this->factory->user->create();
+		$this->set_current_user( $u1 );
+
+		// Create a couple of optouts.
+		$args = array(
+			'email_address'     => 'one@wp.org',
+			'user_id'           => $u1,
+			'email_type'        => 'annoyance'
+		);
+		$i1 = bp_add_optout( $args );
+		$args['email_address'] = 'two@wp.org';
+		$i2 = bp_add_optout( $args );
+
+		$get_args = array(
+			'user_id'        => $u1,
+			'fields'         => 'ids',
+		);
+		$optouts = bp_get_optouts( $get_args );
+		$this->assertEqualSets( array( $i1, $i2 ), $optouts );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	public function test_bp_optouts_add_optout_avoid_duplicates() {
+		$old_current_user = get_current_user_id();
+
+		$u1 = $this->factory->user->create();
+		$this->set_current_user( $u1 );
+
+		// Create an optouts.
+		$args = array(
+			'email_address'     => 'one@wp.org',
+			'user_id'           => $u1,
+			'email_type'        => 'annoyance'
+		);
+		$i1 = bp_add_optout( $args );
+		// Attempt to create a duplicate. Should return existing optout id.
+		$i2 = bp_add_optout( $args );
+		$this->assertEquals( $i1, $i2 );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	public function test_bp_optouts_delete_optout() {
+		$old_current_user = get_current_user_id();
+
+		$u1 = $this->factory->user->create();
+		$this->set_current_user( $u1 );
+
+		$args = array(
+			'email_address'     => 'one@wp.org',
+			'user_id'           => $u1,
+			'email_type'        => 'annoyance'
+		);
+		$i1 = bp_add_optout( $args );
+		bp_delete_optout_by_id( $i1 );
+
+		$get_args = array(
+			'user_id'        => $u1,
+			'fields'         => 'ids',
+		);
+		$optouts = bp_get_optouts( $get_args );
+		$this->assertTrue( empty( $optouts ) );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+	public function test_bp_optouts_get_by_search_terms() {
+		$old_current_user = get_current_user_id();
+
+		$u1 = $this->factory->user->create();
+		$this->set_current_user( $u1 );
+
+		// Create a couple of optouts.
+		$args = array(
+			'email_address'     => 'one@wpfrost.org',
+			'user_id'           => $u1,
+			'email_type'        => 'annoyance'
+		);
+		$i1 = bp_add_optout( $args );
+		$args['email_address'] = 'two@wp.org';
+		$i2 = bp_add_optout( $args );
+
+		$get_args = array(
+			'search_terms'   => 'one@wpfrost.org',
+			'fields'         => 'ids',
+		);
+		$optouts = bp_get_optouts( $get_args );
+		$this->assertEqualSets( array( $i1 ), $optouts );
+
+		$this->set_current_user( $old_current_user );
+	}
+
+}
