Custom Post Type permalink without /%day%/ and/or /%postname%/

I’m not sure how your existing setup is working, since I can’t reproduce the month/year archive behavior using your code with the default twentysixteen theme, however, the solution is perhaps the same, but perhaps not.

One thing first- I recommend against using the post publish date for the year/month, it just limits your flexibility. I’m going to use post metadata to store the year and month, under the keys webinar_year and webinar_month. You can add a custom meta box for these if you want to make it simple for end users, and you can also hook into the post save process and introduce some automation.

So the first step is to add a couple of new rewrite tags and define a new permastruct for your post type-

function cptui_register_my_cpts_webinars() {

    // your existing post type registration code
    // $args, etc..
    register_post_type( "webinars", $args );

    add_rewrite_tag( '%webinar_year%', '([0-9]{4})' );
    add_rewrite_tag( '%webinar_month%', '([0-9]{2})' );
    add_permastruct( 'webinars', 'webinar/%webinar_year%/%webinar_month%/' );

}
add_action( 'init', 'cptui_register_my_cpts_webinars' );

Don’t forget to flush rewrite rules after you add this.

Then you’ll need a filter on the post link to substitute the webinar_year and webinar_month meta values for the placeholders-

function webinars_permalink( $post_link, $post ) {
    if( 'webinars' == $post->post_type ) {
        $webinar_year = ( ! get_post_meta( $post->ID, 'webinar_year', true ) ) ? '0000' : get_post_meta( $post->ID, 'webinar_year', true );
        $webinar_month = ( ! get_post_meta( $post->ID, 'webinar_month', true ) ) ? '00' : get_post_meta( $post->ID, 'webinar_month', true );
        $post_link = str_replace( ['%webinar_year%', '%webinar_month%'], [$webinar_year, $webinar_month], $post_link );
    }
    return $post_link;
}
add_filter( 'post_type_link', 'webinars_permalink', 10, 2 );

Now you should be able to create these posts and see the correct URL with year and month values substituted. But they still won’t work when you visit them.

These requests are currently meaningless to WordPress, so we need to intercept them, figure out what post they are for, then correct the query so WordPress can find it. We do this with a filter on request, which fires early, before WordPress makes any decisions about what the query is for-

function webinars_request( $request ) {
    if( isset( $request['webinar_year'] ) && isset( $request['webinar_month'] ) ){
        $webinar = new WP_Query([
            'post_type' => 'webinars',
            'posts_per_page' => 1,
            'meta_query' => [
                [
                    'key' => 'webinar_year',
                    'value' => $request['webinar_year']
                ],
                [
                    'key' => 'webinar_month',
                    'value' => $request['webinar_month']
                ]
            ]
        ]);
        if( $webinar->have_posts() ){
            $request['post_type'] = 'webinars';
            $request['webinars'] = $webinar->post->post_name;
            $request['name'] = $webinar->post->post_name;
        }
    }
    return $request;
}
add_filter( 'request', 'webinars_request' );

Here we’re just querying for one post with the matching meta keys/values. It’s going to return the most recent one with the requested values, so enforcement of uniqueness here is on you.

No, it’s not the most elegant. WordPress only treats post slug as the unique thing it identifies a singular post query by, so anything else you want to shoehorn in there is a bit of a manual endeavor.